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:
-rw-r--r--.gitlab/ci/preflight.gitlab-ci.yml10
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml4
-rw-r--r--.rubocop.yml3
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml25
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/blob/csv/index.js1
-rw-r--r--app/assets/javascripts/work_items/components/notes/activity_sort.vue99
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue (renamed from app/assets/javascripts/work_items/components/notes/activity_filter.vue)79
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue46
-rw-r--r--app/assets/javascripts/work_items/constants.js25
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb8
-rw-r--r--app/controllers/concerns/enforces_two_factor_authentication.rb3
-rw-r--r--app/controllers/concerns/issuable_actions.rb18
-rw-r--r--app/controllers/oauth/authorizations_controller.rb8
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb4
-rw-r--r--app/controllers/oauth/jira_dvcs/authorizations_controller.rb10
-rw-r--r--app/controllers/projects/pipelines_controller.rb5
-rw-r--r--app/controllers/sessions_controller.rb3
-rw-r--r--app/models/concerns/each_batch.rb76
-rw-r--r--app/models/concerns/web_hooks/auto_disabling.rb82
-rw-r--r--app/models/concerns/web_hooks/unstoppable.rb29
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/service_hook.rb1
-rw-r--r--app/models/hooks/system_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb49
-rw-r--r--app/models/namespace.rb38
-rw-r--r--app/models/pages/lookup_path.rb14
-rw-r--r--app/models/pages_domain.rb18
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/resource_milestone_event.rb4
-rw-r--r--app/views/projects/blob/viewers/_csv.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos_list.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml23
-rw-r--r--app/views/projects/pipelines/_pipeline_stats_text.html.haml1
-rw-r--r--config/feature_flags/development/disabled_mr_discussions_redis_cache.yml8
-rw-r--r--config/feature_flags/ops/auto_disabling_web_hooks.yml8
-rw-r--r--config/metrics/counts_28d/20210216180321_action_monthly_active_users_sfe_edit.yml4
-rw-r--r--config/metrics/counts_28d/20210216180323_action_monthly_active_users_snippet_editor_edit.yml4
-rw-r--r--doc/administration/geo/replication/img/adding_a_secondary_v15_8.pngbin40126 -> 14698 bytes
-rw-r--r--doc/architecture/blueprints/_template.md1
-rw-r--r--doc/development/advanced_search.md10
-rw-r--r--doc/development/contributing/index.md8
-rw-r--r--doc/development/contributing/merge_request_workflow.md11
-rw-r--r--doc/development/fe_guide/vuex.md9
-rw-r--r--doc/operations/error_tracking.md58
-rw-r--r--doc/operations/img/Monitor-list_errors.pngbin118463 -> 0 bytes
-rw-r--r--doc/operations/img/Monitor_tab-post-enable.pngbin193554 -> 0 bytes
-rw-r--r--doc/operations/img/Monitor_tab-pre-enable.pngbin188656 -> 0 bytes
-rw-r--r--doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.pngbin35066 -> 12050 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.pngbin73370 -> 22361 bytes
-rw-r--r--doc/user/group/epics/manage_epics.md6
-rw-r--r--doc/user/group/saml_sso/index.md32
-rw-r--r--lib/api/entities/internal/pages/lookup_path.rb8
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/internal/pages.rb21
-rw-r--r--lib/gitlab/database/migration_helpers.rb47
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb71
-rw-r--r--lib/gitlab/pages/virtual_host_finder.rb72
-rw-r--r--lib/gitlab/usage_data.rb15
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json2
-rw-r--r--rubocop/rubocop-ruby30.yml4
-rw-r--r--rubocop/rubocop-ruby31.yml10
-rwxr-xr-xscripts/lint-docs-blueprints.rb2
-rw-r--r--scripts/utils.sh14
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb3
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb22
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb44
-rw-r--r--spec/fixtures/api/schemas/internal/pages/lookup_path.json3
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js11
-rw-r--r--spec/frontend/work_items/components/notes/activity_filter_spec.js83
-rw-r--r--spec/frontend/work_items/components/notes/activity_sort_spec.js69
-rw-r--r--spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js109
-rw-r--r--spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js10
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb75
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb122
-rw-r--r--spec/lib/gitlab/pages/virtual_host_finder_spec.rb214
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb35
-rw-r--r--spec/models/concerns/each_batch_spec.rb32
-rw-r--r--spec/models/namespace_spec.rb65
-rw-r--r--spec/models/pages/lookup_path_spec.rb69
-rw-r--r--spec/models/pages_domain_spec.rb38
-rw-r--r--spec/requests/api/files_spec.rb6
-rw-r--r--spec/requests/api/internal/pages_spec.rb99
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb261
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb90
-rw-r--r--yarn.lock61
87 files changed, 1601 insertions, 1069 deletions
diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml
index 464eac56a95..8c1cb44807a 100644
--- a/.gitlab/ci/preflight.gitlab-ci.yml
+++ b/.gitlab/ci/preflight.gitlab-ci.yml
@@ -16,20 +16,24 @@
- !reference [.default-before_script, before_script]
- cd qa && bundle install
-rails-production-environment:
+rails-production-server-boot:
extends:
- .preflight-job-base
- .default-before_script
- .production
- .ruby-cache
- - .setup:rules:rails-production-environment
+ - .setup:rules:rails-production-server-boot
- .use-pg12
variables:
BUNDLE_WITHOUT: "development:test"
BUNDLE_WITH: "production"
needs: []
script:
- - bundle exec rails runner --environment=production 'puts Rails.env'
+ - source scripts/utils.sh
+ - bundle exec rails server -e production &
+ - sleep 40 # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114124#note_1309506358
+ - retry_times_sleep 10 5 "curl http://0.0.0.0:3000"
+ - kill $(jobs -p)
no-ee-check:
extends:
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 3d85325382d..b09fd5e2aa6 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -349,11 +349,13 @@
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/background_migration/**/*"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/database{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/database/**/*"
+ - "{,ee/,jh/}spec/support/db_cleaner.rb"
- "{,ee/,jh/}spec/support/helpers/database/**/*"
- "{,ee/,jh/}spec/support/helpers/migrations_helpers/**/*"
- "lib/api/admin/batched_background_migrations.rb"
- "lib/gitlab/markdown_cache/active_record/**/*"
- "spec/requests/api/admin/batched_background_migrations_spec.rb"
+ - "spec/support/database_cleaner.rb"
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
- "{,ee/,jh/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
# Gitaly has interactions with background migrations: https://gitlab.com/gitlab-org/gitlab/-/issues/336538
@@ -2221,7 +2223,7 @@
- <<: *if-default-refs
changes: *code-backstage-patterns
-.setup:rules:rails-production-environment:
+.setup:rules:rails-production-server-boot:
rules:
- <<: *if-default-refs
changes: *code-patterns
diff --git a/.rubocop.yml b/.rubocop.yml
index 2de8a88633b..56c51bee0e3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -19,6 +19,9 @@ inherit_from:
<% if RUBY_VERSION[/^\d+\.\d+/, 0] == '3.0' %>
- ./rubocop/rubocop-ruby30.yml
<% end %>
+ <% if RUBY_VERSION[/^\d+\.\d+/, 0] == '3.1' %>
+ - ./rubocop/rubocop-ruby31.yml
+ <% end %>
- ./rubocop/rubocop-migrations.yml
- ./rubocop/rubocop-usage-data.yml
- ./rubocop/rubocop-code_reuse.yml
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 75b7808e17e..1249df0bd64 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -3,12 +3,6 @@
Layout/ArgumentAlignment:
Details: grace period
Exclude:
- - 'app/controllers/concerns/authenticates_with_two_factor.rb'
- - 'app/controllers/concerns/enforces_two_factor_authentication.rb'
- - 'app/controllers/oauth/authorizations_controller.rb'
- - 'app/controllers/oauth/authorized_applications_controller.rb'
- - 'app/controllers/oauth/jira_dvcs/authorizations_controller.rb'
- - 'app/controllers/sessions_controller.rb'
- 'app/finders/autocomplete/users_finder.rb'
- 'app/finders/group_descendants_finder.rb'
- 'app/graphql/mutations/achievements/create.rb'
@@ -864,22 +858,6 @@ Layout/ArgumentAlignment:
- 'ee/app/components/namespaces/free_user_cap/base_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/enforcement_at_limit_alert_component.rb'
- 'ee/app/components/namespaces/free_user_cap/shared.rb'
- - 'ee/app/controllers/admin/credentials_controller.rb'
- - 'ee/app/controllers/concerns/ee/analytics/cycle_analytics/stage_actions.rb'
- - 'ee/app/controllers/ee/groups_controller.rb'
- - 'ee/app/controllers/ee/passwords_controller.rb'
- - 'ee/app/controllers/ee/registrations_controller.rb'
- - 'ee/app/controllers/ee/search_controller.rb'
- - 'ee/app/controllers/groups/analytics/ci_cd_analytics_controller.rb'
- - 'ee/app/controllers/groups/analytics/cycle_analytics_controller.rb'
- - 'ee/app/controllers/groups/saml_group_links_controller.rb'
- - 'ee/app/controllers/groups/security/credentials_controller.rb'
- - 'ee/app/controllers/groups/two_factor_auths_controller.rb'
- - 'ee/app/controllers/omniauth_kerberos_controller.rb'
- - 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb'
- - 'ee/app/controllers/registrations/groups_projects_controller.rb'
- - 'ee/app/controllers/subscriptions_controller.rb'
- - 'ee/app/controllers/trials_controller.rb'
- 'ee/app/finders/security/findings_finder.rb'
- 'ee/app/finders/security/training_providers/base_url_finder.rb'
- 'ee/app/graphql/ee/mutations/alert_management/http_integration/create.rb'
@@ -2183,9 +2161,6 @@ Layout/ArgumentAlignment:
- 'spec/components/previews/pajamas/alert_component_preview.rb'
- 'spec/components/previews/pajamas/banner_component_preview.rb'
- 'spec/components/previews/pajamas/button_component_preview.rb'
- - 'spec/controllers/oauth/authorizations_controller_spec.rb'
- - 'spec/controllers/oauth/jira_dvcs/authorizations_controller_spec.rb'
- - 'spec/controllers/omniauth_callbacks_controller_spec.rb'
- 'spec/factories/ci/processable.rb'
- 'spec/factories/draft_note.rb'
- 'spec/factories/environments.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 43c96484a51..69d1e4c4105 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-895cd7e481bd7c07b1f97d6ba9cd3aef76ea637d
+4fe33cae7dca4ca605d0f505743ba4aa861fa876
diff --git a/app/assets/javascripts/blob/csv/index.js b/app/assets/javascripts/blob/csv/index.js
index 4cf6c169c68..ed8e1ffa318 100644
--- a/app/assets/javascripts/blob/csv/index.js
+++ b/app/assets/javascripts/blob/csv/index.js
@@ -10,6 +10,7 @@ export default () => {
return createElement(CsvViewer, {
props: {
csv: el.dataset.data,
+ remoteFile: true,
},
});
},
diff --git a/app/assets/javascripts/work_items/components/notes/activity_sort.vue b/app/assets/javascripts/work_items/components/notes/activity_sort.vue
deleted file mode 100644
index bfbb2b65346..00000000000
--- a/app/assets/javascripts/work_items/components/notes/activity_sort.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { ASC, DESC } from '~/notes/constants';
-import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants';
-
-const sortOptions = [
- { key: DESC, text: __('Newest first'), testid: 'newest-first' },
- { key: ASC, text: __('Oldest first') },
-];
-
-export default {
- sortOptions,
- components: {
- GlDropdown,
- GlDropdownItem,
- LocalStorageSync,
- },
- mixins: [Tracking.mixin()],
- props: {
- sortOrder: {
- type: String,
- default: ASC,
- required: false,
- },
- loading: {
- type: Boolean,
- default: false,
- required: false,
- },
- workItemType: {
- type: String,
- required: true,
- },
- },
- computed: {
- tracking() {
- return {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_sorting',
- property: `type_${this.workItemType}`,
- };
- },
- selectedSortOption() {
- return sortOptions.find(({ key }) => this.sortOrder === key) || ASC;
- },
- getDropdownSelectedText() {
- return this.selectedSortOption.text;
- },
- },
- methods: {
- setDiscussionSortDirection(direction) {
- this.$emit('changeSort', direction);
- },
- fetchSortedDiscussions(direction) {
- if (this.isSortDropdownItemActive(direction)) {
- return;
- }
- this.track('work_item_notes_sort_order_changed');
- this.$emit('changeSort', direction);
- },
- isSortDropdownItemActive(sortDir) {
- return sortDir === this.sortOrder;
- },
- },
- WORK_ITEM_NOTES_SORT_ORDER_KEY,
-};
-</script>
-
-<template>
- <div class="gl-display-inline-block gl-vertical-align-bottom">
- <local-storage-sync
- :value="sortOrder"
- :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
- as-string
- @input="setDiscussionSortDirection"
- />
- <gl-dropdown
- class="gl-xs-w-full"
- size="small"
- :text="getDropdownSelectedText"
- :disabled="loading"
- right
- >
- <gl-dropdown-item
- v-for="{ text, key, testid } in $options.sortOptions"
- :key="text"
- :data-testid="testid"
- is-check-item
- :is-checked="isSortDropdownItemActive(key)"
- @click="fetchSortedDiscussions(key)"
- >
- {{ text }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
-</template>
diff --git a/app/assets/javascripts/work_items/components/notes/activity_filter.vue b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
index 6d5535797ef..1ead16c944b 100644
--- a/app/assets/javascripts/work_items/components/notes/activity_filter.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
@@ -1,35 +1,10 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { s__ } from '~/locale';
import Tracking from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import {
- WORK_ITEM_NOTES_FILTER_ALL_NOTES,
- WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
- WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
- TRACKING_CATEGORY_SHOW,
- WORK_ITEM_NOTES_FILTER_KEY,
-} from '~/work_items/constants';
-
-const filterOptions = [
- {
- key: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
- text: s__('WorkItem|All activity'),
- },
- {
- key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
- text: s__('WorkItem|Comments only'),
- testid: 'comments-activity',
- },
- {
- key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
- text: s__('WorkItem|History only'),
- testid: 'history-activity',
- },
-];
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
export default {
- filterOptions,
components: {
GlDropdown,
GlDropdownItem,
@@ -46,17 +21,40 @@ export default {
type: String,
required: true,
},
- discussionFilter: {
+ sortFilterProp: {
type: String,
- default: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
- required: false,
+ required: true,
+ },
+ filterOptions: {
+ type: Array,
+ required: true,
+ },
+ trackingLabel: {
+ type: String,
+ required: true,
+ },
+ trackingAction: {
+ type: String,
+ required: true,
+ },
+ filterEvent: {
+ type: String,
+ required: true,
+ },
+ defaultSortFilterProp: {
+ type: String,
+ required: true,
+ },
+ storageKey: {
+ type: String,
+ required: true,
},
},
computed: {
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_filtering',
+ label: this.trackingLabel,
property: `type_${this.workItemType}`,
};
},
@@ -65,35 +63,34 @@ export default {
},
selectedSortOption() {
return (
- filterOptions.find(({ key }) => this.discussionFilter === key) ||
- WORK_ITEM_NOTES_FILTER_ALL_NOTES
+ this.filterOptions.find(({ key }) => this.sortFilterProp === key) ||
+ this.defaultSortFilterProp
);
},
},
methods: {
setDiscussionFilterOption(filterValue) {
- this.$emit('changeFilter', filterValue);
+ this.$emit(this.filterEvent, filterValue);
},
fetchFilteredDiscussions(filterValue) {
if (this.isSortDropdownItemActive(filterValue)) {
return;
}
- this.track('work_item_notes_filter_changed');
- this.$emit('changeFilter', filterValue);
+ this.track(this.trackingAction);
+ this.$emit(this.filterEvent, filterValue);
},
- isSortDropdownItemActive(discussionFilter) {
- return discussionFilter === this.discussionFilter;
+ isSortDropdownItemActive(value) {
+ return value === this.sortFilterProp;
},
},
- WORK_ITEM_NOTES_FILTER_KEY,
};
</script>
<template>
<div class="gl-display-inline-block gl-vertical-align-bottom">
<local-storage-sync
- :value="discussionFilter"
- :storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
+ :value="sortFilterProp"
+ :storage-key="storageKey"
as-string
@input="setDiscussionFilterOption"
/>
@@ -105,7 +102,7 @@ export default {
right
>
<gl-dropdown-item
- v-for="{ text, key, testid } in $options.filterOptions"
+ v-for="{ text, key, testid } in filterOptions"
:key="text"
:data-testid="testid"
is-check-item
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 e700d5353e2..0c1419e983f 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
@@ -1,17 +1,21 @@
<script>
-import ActivitySort from '~/work_items/components/notes/activity_sort.vue';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
+import WorkItemActivitySortFilter from '~/work_items/components/notes/work_item_activity_sort_filter.vue';
import { s__ } from '~/locale';
import { ASC } from '~/notes/constants';
-import { WORK_ITEM_NOTES_FILTER_ALL_NOTES } from '~/work_items/constants';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_ACTIVITY_FILTER_OPTIONS,
+ WORK_ITEM_NOTES_FILTER_KEY,
+ WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ WORK_ITEM_NOTES_SORT_ORDER_KEY,
+} from '~/work_items/constants';
export default {
i18n: {
activityLabel: s__('WorkItem|Activity'),
},
components: {
- ActivitySort,
- ActivityFilter,
+ WorkItemActivitySortFilter,
},
props: {
disableActivityFilterSort: {
@@ -41,6 +45,12 @@ export default {
this.$emit('changeFilter', filterValue);
},
},
+ WORK_ITEM_ACTIVITY_FILTER_OPTIONS,
+ WORK_ITEM_NOTES_FILTER_KEY,
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ ASC,
};
</script>
@@ -50,16 +60,30 @@ export default {
>
<h3 class="gl-font-base gl-m-0">{{ $options.i18n.activityLabel }}</h3>
<div class="gl-display-flex gl-gap-3">
- <activity-filter
- :loading="disableActivityFilterSort"
+ <work-item-activity-sort-filter
:work-item-type="workItemType"
- :discussion-filter="discussionFilter"
+ :loading="disableActivityFilterSort"
+ :sort-filter-prop="discussionFilter"
+ :filter-options="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
+ :storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
+ :default-sort-filter-prop="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
+ tracking-action="work_item_notes_filter_changed"
+ tracking-label="item_track_notes_filtering"
+ filter-event="changeFilter"
+ data-testid="work-item-filter"
@changeFilter="filterDiscussions"
/>
- <activity-sort
- :loading="disableActivityFilterSort"
- :sort-order="sortOrder"
+ <work-item-activity-sort-filter
:work-item-type="workItemType"
+ :loading="disableActivityFilterSort"
+ :sort-filter-prop="sortOrder"
+ :filter-options="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
+ :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
+ :default-sort-filter-prop="$options.ASC"
+ tracking-action="work_item_notes_sort_order_changed"
+ tracking-label="item_track_notes_sorting"
+ filter-event="changeSort"
+ data-testid="work-item-sort"
@changeSort="changeNotesSortOrder"
/>
</div>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index b372f2d6f7b..bbcf78e23aa 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -1,5 +1,6 @@
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { ASC, DESC } from '~/notes/constants';
export const STATE_OPEN = 'OPEN';
export const STATE_CLOSED = 'CLOSED';
@@ -182,3 +183,25 @@ export const WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS = 'ONLY_COMMENTS';
export const WORK_ITEM_NOTES_FILTER_ONLY_HISTORY = 'ONLY_HISTORY';
export const WORK_ITEM_NOTES_FILTER_KEY = 'filter_key_work_item';
+
+export const WORK_ITEM_ACTIVITY_FILTER_OPTIONS = [
+ {
+ key: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ text: s__('WorkItem|All activity'),
+ },
+ {
+ key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ text: s__('WorkItem|Comments only'),
+ testid: 'comments-activity',
+ },
+ {
+ key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+ text: s__('WorkItem|History only'),
+ testid: 'history-activity',
+ },
+];
+
+export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [
+ { key: DESC, text: __('Newest first'), testid: 'newest-first' },
+ { key: ASC, text: __('Oldest first') },
+];
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index f5400dcd473..691b4f4e21f 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -114,9 +114,11 @@ module AuthenticatesWithTwoFactor
webauthn_registration_ids = user.webauthn_registrations.pluck(:credential_xid)
- get_options = WebAuthn::Credential.options_for_get(allow: webauthn_registration_ids,
- user_verification: 'discouraged',
- extensions: { appid: WebAuthn.configuration.origin })
+ get_options = WebAuthn::Credential.options_for_get(
+ allow: webauthn_registration_ids,
+ user_verification: 'discouraged',
+ extensions: { appid: WebAuthn.configuration.origin }
+ )
session[:challenge] = get_options.challenge
gon.push(webauthn: { options: Gitlab::Json.dump(get_options) })
end
diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb
index cdef1a45a27..8068913eea2 100644
--- a/app/controllers/concerns/enforces_two_factor_authentication.rb
+++ b/app/controllers/concerns/enforces_two_factor_authentication.rb
@@ -27,7 +27,8 @@ module EnforcesTwoFactorAuthentication
render_error(
format(
_("Authentication error: enable 2FA in your profile settings to continue using GitLab: %{mfa_help_page}"),
- mfa_help_page: mfa_help_page_url),
+ mfa_help_page: mfa_help_page_url
+ ),
status: :unauthorized
)
else
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index f0e8f180eb3..d364daf93c3 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -151,9 +151,7 @@ module IssuableActions
end
case issuable
- when MergeRequest
- render_mr_discussions(discussion_notes, discussion_serializer, discussion_cache_context)
- when Issue
+ when MergeRequest, Issue
if stale?(etag: [discussion_cache_context, discussion_notes])
render json: discussion_serializer.represent(discussion_notes, context: self)
end
@@ -164,20 +162,6 @@ module IssuableActions
private
- def render_mr_discussions(discussions, serializer, cache_context)
- return unless stale?(etag: [cache_context, discussions])
-
- if Feature.enabled?(:disabled_mr_discussions_redis_cache, project)
- render json: serializer.represent(discussions, context: self)
- else
- render_cached_discussions(discussions, serializer, cache_context)
- end
- end
-
- def render_cached_discussions(discussions, serializer, cache_context)
- render_cached(discussions, with: serializer, cache_context: ->(_) { cache_context }, context: self)
- end
-
def notes_filter
strong_memoize(:notes_filter) do
notes_filter_param = params[:notes_filter]&.to_i
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 43bf895ea76..96a3fab7e1a 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -108,8 +108,10 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
end
def dangerous_scopes?
- doorkeeper_application&.includes_scope?(*::Gitlab::Auth::API_SCOPE, *::Gitlab::Auth::READ_API_SCOPE,
- *::Gitlab::Auth::ADMIN_SCOPES, *::Gitlab::Auth::REPOSITORY_SCOPES,
- *::Gitlab::Auth::REGISTRY_SCOPES) && !doorkeeper_application&.trusted?
+ doorkeeper_application&.includes_scope?(
+ *::Gitlab::Auth::API_SCOPE, *::Gitlab::Auth::READ_API_SCOPE,
+ *::Gitlab::Auth::ADMIN_SCOPES, *::Gitlab::Auth::REPOSITORY_SCOPES,
+ *::Gitlab::Auth::REGISTRY_SCOPES
+ ) && !doorkeeper_application&.trusted?
end
end
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 3f476c0d717..6fc2eb6bc45 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -20,7 +20,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
end
redirect_to applications_profile_url,
- status: :found,
- notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
+ status: :found,
+ notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
end
end
diff --git a/app/controllers/oauth/jira_dvcs/authorizations_controller.rb b/app/controllers/oauth/jira_dvcs/authorizations_controller.rb
index 03921761f45..82a6784d2d1 100644
--- a/app/controllers/oauth/jira_dvcs/authorizations_controller.rb
+++ b/app/controllers/oauth/jira_dvcs/authorizations_controller.rb
@@ -16,10 +16,12 @@ class Oauth::JiraDvcs::AuthorizationsController < ApplicationController
def new
session[:redirect_uri] = params['redirect_uri']
- redirect_to oauth_authorization_path(client_id: params['client_id'],
- response_type: 'code',
- scope: normalize_scope(params['scope']),
- redirect_uri: oauth_jira_dvcs_callback_url)
+ redirect_to oauth_authorization_path(
+ client_id: params['client_id'],
+ response_type: 'code',
+ scope: normalize_scope(params['scope']),
+ redirect_uri: oauth_jira_dvcs_callback_url
+ )
end
# 2. Handle the callback call as we were a Github Enterprise instance client.
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 6d84cb20983..e3b1b3dd2a8 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -22,6 +22,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy]
+ before_action :push_frontend_feature_flags, only: [:show]
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
@@ -364,6 +365,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def tracking_project_source
project
end
+
+ def push_frontend_feature_flags
+ push_frontend_feature_flag(:refactor_ci_minutes_consumption, @project)
+ end
end
Projects::PipelinesController.prepend_mod_with('Projects::PipelinesController')
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1003de314d2..8a79353f490 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -69,8 +69,7 @@ class SessionsController < Devise::SessionsController
super do |resource|
# User has successfully signed in, so clear any unused reset token
if resource.reset_password_token.present?
- resource.update(reset_password_token: nil,
- reset_password_sent_at: nil)
+ resource.update(reset_password_token: nil, reset_password_sent_at: nil)
end
if resource.deactivated?
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index dbc0887dc97..79fb81e7820 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -161,5 +161,81 @@ module EachBatch
break unless stop
end
end
+
+ # Iterates over the relation and counts the rows. The counting
+ # logic is combined with the iteration query which saves one query
+ # compared to a standard each_batch approach.
+ #
+ # Basic usage:
+ # count, _last_value = Project.each_batch_count
+ #
+ # The counting can be stopped by passing a block and making the last statement true.
+ # Example:
+ #
+ # query_count = 0
+ # count, last_value = Project.each_batch_count do
+ # query_count += 1
+ # query_count == 5 # stop counting after 5 loops
+ # end
+ #
+ # Resume where the previous counting has stopped:
+ #
+ # count, last_value = Project.each_batch_count(last_count: count, last_value: last_value)
+ #
+ # Another example, counting issues in project:
+ #
+ # project = Project.find(1)
+ # count, _ = project.issues.each_batch_count(column: :iid)
+ def each_batch_count(of: 1000, column: :id, last_count: 0, last_value: nil)
+ arel_table = self.arel_table
+ window = Arel::Nodes::Window.new.order(arel_table[column])
+ last_value_column = Arel::Nodes::NamedFunction
+ .new('LAST_VALUE', [arel_table[column]])
+ .over(window)
+ .as(column.to_s)
+
+ loop do
+ count_column = Arel::Nodes::Addition
+ .new(Arel::Nodes::NamedFunction.new('ROW_NUMBER', []).over(window), last_count)
+ .as('count')
+
+ projections = [count_column, last_value_column]
+ scope = limit(1).offset(of - 1)
+ scope = scope.where(arel_table[column].gt(last_value)) if last_value
+ new_count, last_value = scope.pick(*projections)
+
+ # When reaching the last batch the offset query might return no data, to address this
+ # problem, we invoke a specialized query that takes the last row out of the resultset.
+ # We could do this for each batch, however it would add unnecessary overhead to all
+ # queries.
+ if new_count.nil?
+ inner_query = scope
+ .select(*projections)
+ .limit(nil)
+ .offset(nil)
+ .arel
+ .as(quoted_table_name)
+
+ new_count, last_value =
+ unscoped
+ .from(inner_query)
+ .order(count: :desc)
+ .limit(1)
+ .pick(:count, column)
+
+ last_count = new_count if new_count
+ last_value = nil
+ break
+ end
+
+ last_count = new_count
+
+ if block_given?
+ should_break = yield(last_count, last_value)
+ break if should_break
+ end
+ end
+ [last_count, last_value]
+ end
end
end
diff --git a/app/models/concerns/web_hooks/auto_disabling.rb b/app/models/concerns/web_hooks/auto_disabling.rb
index 2cc17a6f185..5f9901901d4 100644
--- a/app/models/concerns/web_hooks/auto_disabling.rb
+++ b/app/models/concerns/web_hooks/auto_disabling.rb
@@ -4,7 +4,20 @@ module WebHooks
module AutoDisabling
extend ActiveSupport::Concern
+ ENABLED_HOOK_TYPES = %w[ProjectHook].freeze
+
+ class_methods do
+ def auto_disabling_enabled?
+ ENABLED_HOOK_TYPES.include?(name) &&
+ Gitlab::SafeRequestStore.fetch(:auto_disabling_web_hooks) do
+ Feature.enabled?(:auto_disabling_web_hooks, type: :ops)
+ end
+ end
+ end
+
included do
+ delegate :auto_disabling_enabled?, to: :class, private: true
+
# A hook is disabled if:
#
# - we are no longer in the grace-perod (recent_failures > ?)
@@ -12,6 +25,8 @@ module WebHooks
# - disabled_until is nil (i.e. this was set by WebHook#fail!)
# - or disabled_until is in the future (i.e. this was set by WebHook#backoff!)
scope :disabled, -> do
+ return none unless auto_disabling_enabled?
+
where('recent_failures > ? AND (disabled_until IS NULL OR disabled_until >= ?)',
WebHook::FAILURE_THRESHOLD, Time.current)
end
@@ -23,40 +38,83 @@ module WebHooks
# - disabled_until is nil (i.e. this was set by WebHook#fail!)
# - disabled_until is in the future (i.e. this was set by WebHook#backoff!)
scope :executable, -> do
+ return all unless auto_disabling_enabled?
+
where('recent_failures <= ? OR (recent_failures > ? AND (disabled_until IS NOT NULL) AND (disabled_until < ?))',
WebHook::FAILURE_THRESHOLD, WebHook::FAILURE_THRESHOLD, Time.current)
end
end
def executable?
+ return true unless auto_disabling_enabled?
+
!temporarily_disabled? && !permanently_disabled?
end
def temporarily_disabled?
- return false if recent_failures <= WebHook::FAILURE_THRESHOLD
+ return false unless auto_disabling_enabled?
- disabled_until.present? && disabled_until >= Time.current
+ disabled_until.present? && disabled_until >= Time.current && recent_failures > WebHook::FAILURE_THRESHOLD
end
def permanently_disabled?
- return false if disabled_until.present?
+ return false unless auto_disabling_enabled?
- recent_failures > WebHook::FAILURE_THRESHOLD
+ recent_failures > WebHook::FAILURE_THRESHOLD && disabled_until.blank?
end
def disable!
- return if permanently_disabled?
+ return if !auto_disabling_enabled? || permanently_disabled?
+
+ update_attribute(:recent_failures, WebHook::EXCEEDED_FAILURE_THRESHOLD)
+ end
+
+ def enable!
+ return unless auto_disabling_enabled?
+ return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0
- super
+ assign_attributes(recent_failures: 0, disabled_until: nil, backoff_count: 0)
+ save(validate: false)
end
+ # Don't actually back-off until FAILURE_THRESHOLD failures have been seen
+ # we mark the grace-period using the recent_failures counter
def backoff!
+ return unless auto_disabling_enabled?
return if permanently_disabled? || (backoff_count >= WebHook::MAX_FAILURES && temporarily_disabled?)
- super
+ attrs = { recent_failures: next_failure_count }
+
+ if recent_failures >= WebHook::FAILURE_THRESHOLD
+ attrs[:backoff_count] = next_backoff_count
+ attrs[:disabled_until] = next_backoff.from_now
+ end
+
+ assign_attributes(attrs)
+ save(validate: false) if changed?
+ end
+
+ def failed!
+ return unless auto_disabling_enabled?
+ return unless recent_failures < WebHook::MAX_FAILURES
+
+ assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: next_failure_count)
+ save(validate: false)
+ end
+
+ def next_backoff
+ if backoff_count >= 8 # optimization to prevent expensive exponentiation and possible overflows
+ return WebHook::MAX_BACKOFF
+ end
+
+ (WebHook::INITIAL_BACKOFF * (WebHook::BACKOFF_GROWTH_FACTOR**backoff_count))
+ .clamp(WebHook::INITIAL_BACKOFF, WebHook::MAX_BACKOFF)
+ .seconds
end
def alert_status
+ return :executable unless auto_disabling_enabled?
+
if temporarily_disabled?
:temporarily_disabled
elsif permanently_disabled?
@@ -65,5 +123,15 @@ module WebHooks
:executable
end
end
+
+ private
+
+ def next_failure_count
+ recent_failures.succ.clamp(1, WebHook::MAX_FAILURES)
+ end
+
+ def next_backoff_count
+ backoff_count.succ.clamp(1, WebHook::MAX_FAILURES)
+ end
end
end
diff --git a/app/models/concerns/web_hooks/unstoppable.rb b/app/models/concerns/web_hooks/unstoppable.rb
deleted file mode 100644
index 26284fe3c36..00000000000
--- a/app/models/concerns/web_hooks/unstoppable.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module WebHooks
- module Unstoppable
- extend ActiveSupport::Concern
-
- included do
- scope :executable, -> { all }
-
- scope :disabled, -> { none }
- end
-
- def executable?
- true
- end
-
- def temporarily_disabled?
- false
- end
-
- def permanently_disabled?
- false
- end
-
- def alert_status
- :executable
- end
- end
-end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 8e9a74a68d0..695041f0247 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,7 +2,6 @@
class ProjectHook < WebHook
include TriggerableHooks
- include WebHooks::AutoDisabling
include Presentable
include Limitable
extend ::Gitlab::Utils::Override
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 6af70c249a0..453b986ca4d 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class ServiceHook < WebHook
- include WebHooks::Unstoppable
include Presentable
extend ::Gitlab::Utils::Override
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index eaffe83cab3..3c7f0ef9ffc 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -2,7 +2,6 @@
class SystemHook < WebHook
include TriggerableHooks
- include WebHooks::Unstoppable
triggerable_hooks [
:repository_update_hooks,
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 819152a38c8..ceee86c1671 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -2,6 +2,7 @@
class WebHook < ApplicationRecord
include Sortable
+ include WebHooks::AutoDisabling
InterpolationError = Class.new(StandardError)
@@ -78,46 +79,6 @@ class WebHook < ApplicationRecord
'user/project/integrations/webhooks'
end
- def next_backoff
- return MAX_BACKOFF if backoff_count >= 8 # optimization to prevent expensive exponentiation and possible overflows
-
- (INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**backoff_count))
- .clamp(INITIAL_BACKOFF, MAX_BACKOFF)
- .seconds
- end
-
- def disable!
- update_attribute(:recent_failures, EXCEEDED_FAILURE_THRESHOLD)
- end
-
- def enable!
- return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0
-
- assign_attributes(recent_failures: 0, disabled_until: nil, backoff_count: 0)
- save(validate: false)
- end
-
- # Don't actually back-off until FAILURE_THRESHOLD failures have been seen
- # we mark the grace-period using the recent_failures counter
- def backoff!
- attrs = { recent_failures: next_failure_count }
-
- if recent_failures >= FAILURE_THRESHOLD
- attrs[:backoff_count] = next_backoff_count
- attrs[:disabled_until] = next_backoff.from_now
- end
-
- assign_attributes(attrs)
- save(validate: false) if changed?
- end
-
- def failed!
- return unless recent_failures < MAX_FAILURES
-
- assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: next_failure_count)
- save(validate: false)
- end
-
# @return [Boolean] Whether or not the WebHook is currently throttled.
def rate_limited?
rate_limiter.rate_limited?
@@ -179,14 +140,6 @@ class WebHook < ApplicationRecord
self.url_variables = {} if url_changed? && !encrypted_url_variables_changed?
end
- def next_failure_count
- recent_failures.succ.clamp(1, MAX_FAILURES)
- end
-
- def next_backoff_count
- backoff_count.succ.clamp(1, MAX_FAILURES)
- end
-
def initialize_url_variables
self.url_variables = {} if encrypted_url_variables.nil?
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 2d264e40f33..0b3d4cbbc66 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -254,15 +254,6 @@ class Namespace < ApplicationRecord
value.scan(Gitlab::Regex.group_name_regex_chars).join(' ')
end
- def find_by_pages_host(host)
- gitlab_host = "." + Settings.pages.host.downcase
- host = host.downcase
- return unless host.ends_with?(gitlab_host)
-
- name = host.delete_suffix(gitlab_host)
- Namespace.top_most.by_path(name)
- end
-
def top_most
by_parent(nil)
end
@@ -504,22 +495,6 @@ class Namespace < ApplicationRecord
ContainerRepository.for_project_id(all_projects)
end
- def pages_virtual_domain
- cache = if Feature.enabled?(:cache_pages_domain_api, root_ancestor)
- ::Gitlab::Pages::CacheControl.for_namespace(root_ancestor.id)
- end
-
- Pages::VirtualDomain.new(
- trim_prefix: full_path,
- cache: cache,
- projects: all_projects_with_pages.includes(
- :route,
- :project_setting,
- :project_feature,
- pages_metadatum: :pages_deployment)
- )
- end
-
def any_project_with_pages_deployed?
all_projects.with_pages_deployed.any?
end
@@ -634,6 +609,15 @@ class Namespace < ApplicationRecord
namespace_settings&.all_ancestors_have_runner_registration_enabled?
end
+ def all_projects_with_pages
+ all_projects.with_pages_deployed.includes(
+ :route,
+ :project_setting,
+ :project_feature,
+ pages_metadatum: :pages_deployment
+ )
+ end
+
private
def cross_namespace_reference?(from)
@@ -692,10 +676,6 @@ class Namespace < ApplicationRecord
end
end
- def all_projects_with_pages
- all_projects.with_pages_deployed
- end
-
def parent_changed?
parent_id_changed?
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index a1ba48f3ab0..222cde19da7 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -49,19 +49,25 @@ module Pages
if project.pages_namespace_url == project.pages_url
'/'
else
- project.full_path.delete_prefix(trim_prefix) + '/'
+ "#{project.full_path.delete_prefix(trim_prefix)}/"
end
end
strong_memoize_attr :prefix
+ def unique_domain
+ return unless project.project_setting.pages_unique_domain_enabled?
+
+ project.project_setting.pages_unique_domain
+ end
+ strong_memoize_attr :unique_domain
+
private
attr_reader :project, :trim_prefix, :domain
def deployment
- strong_memoize(:deployment) do
- project.pages_metadatum.pages_deployment
- end
+ project.pages_metadatum.pages_deployment
end
+ strong_memoize_attr :deployment
end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 909658214fd..554753dd4b2 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -209,20 +209,6 @@ class PagesDomain < ApplicationRecord
self.certificate_source = 'gitlab_provided' if attribute_changed?(:key)
end
- def pages_virtual_domain
- return unless pages_deployed?
-
- cache = if Feature.enabled?(:cache_pages_domain_api, project.root_namespace)
- ::Gitlab::Pages::CacheControl.for_domain(id)
- end
-
- Pages::VirtualDomain.new(
- projects: [project],
- domain: self,
- cache: cache
- )
- end
-
def clear_auto_ssl_failure
self.auto_ssl_failed = false
end
@@ -237,14 +223,14 @@ class PagesDomain < ApplicationRecord
end
end
- private
-
def pages_deployed?
return false unless project
project.pages_metadatum&.deployed?
end
+ private
+
def set_verification_code
return if self.verification_code.present?
diff --git a/app/models/project.rb b/app/models/project.rb
index af640bd8296..6d82deb891c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -967,6 +967,15 @@ class Project < ApplicationRecord
def project_features_defaults
PROJECT_FEATURES_DEFAULTS
end
+
+ def by_pages_enabled_unique_domain(domain)
+ without_deleted
+ .joins(:project_setting)
+ .find_by(project_setting: {
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: domain
+ })
+ end
end
def initialize(attributes = nil)
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index def7e91af3f..f3301ee2051 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class ResourceMilestoneEvent < ResourceTimeboxEvent
- include IgnorableColumns
-
belongs_to :milestone
scope :include_relations, -> { includes(:user, milestone: [:project, :group]) }
@@ -10,8 +8,6 @@ class ResourceMilestoneEvent < ResourceTimeboxEvent
# state is used for issue and merge request states.
enum state: Issue.available_states.merge(MergeRequest.available_states)
- ignore_columns %i[reference reference_html cached_markdown_version], remove_with: '13.1', remove_after: '2020-06-22'
-
def milestone_title
milestone&.title
end
diff --git a/app/views/projects/blob/viewers/_csv.html.haml b/app/views/projects/blob/viewers/_csv.html.haml
index 3a58bc9902c..3538ba1dd0d 100644
--- a/app/views/projects/blob/viewers/_csv.html.haml
+++ b/app/views/projects/blob/viewers/_csv.html.haml
@@ -1 +1 @@
-.file-content#js-csv-viewer{ data: { data: viewer.blob.data } }
+.file-content#js-csv-viewer{ data: { data: blob_raw_path } }
diff --git a/app/views/projects/mirrors/_mirror_repos_list.html.haml b/app/views/projects/mirrors/_mirror_repos_list.html.haml
index 46833b5986b..5dbbb72db56 100644
--- a/app/views/projects/mirrors/_mirror_repos_list.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_list.html.haml
@@ -3,11 +3,11 @@
.panel.panel-default
.table-responsive
- if !@project.mirror? && @project.remote_mirrors.count == 0
- .gl-card.gl-mt-5
- .gl-card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mt-5' }) do |c|
+ - c.header do
%strong
= _('Mirrored repositories') + ' (0)'
- .gl-card-body
+ - c.body do
= _('There are currently no mirrored repositories.')
- else
%table.table.push-pull-table
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8f7f0a15e69..7a889570f56 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -11,17 +11,20 @@
= preserve(markdown(commit.description, pipeline: :single_line))
.info-well
- .well-segment.pipeline-info{ class: "gl-align-items-baseline!" }
- .icon-container
- = sprite_icon('clock', css_class: 'gl-top-0!')
- - jobs = n_('%d job', '%d jobs', @pipeline.total_size) % @pipeline.total_size
- - if @pipeline.duration
- = s_('Pipelines|%{jobs} %{ref_text} in %{duration}').html_safe % { jobs: jobs, ref_text: @pipeline.ref_text, duration: time_interval_in_words(@pipeline.duration) }
- - else
- = jobs
+ .well-segment.pipeline-info{ class: "gl-align-items-baseline! gl-flex-direction-column" }
+ %div
+ .icon-container
+ = sprite_icon('clock', css_class: 'gl-top-0!')
+ = n_('%d job', '%d jobs', @pipeline.total_size) % @pipeline.total_size
= @pipeline.ref_text
- - if @pipeline.queued_duration
- = s_("Pipelines|(queued for %{queued_duration})") % { queued_duration: time_interval_in_words(@pipeline.queued_duration)}
+ - if @pipeline.finished_at
+ - duration = time_interval_in_words(@pipeline.duration)
+ - queued_duration = time_interval_in_words(@pipeline.queued_duration)
+ %span.gl-pl-7{ 'data-testid': 'pipeline-stats-text' }
+ - if Feature.enabled?(:refactor_ci_minutes_consumption, @project)
+ = render_if_exists 'projects/pipelines/pipeline_stats_text', duration: duration, pipeline: @pipeline, queued_duration: queued_duration
+ - else
+ = s_("in %{duration} and was queued for %{queued_duration}").html_safe % { duration: duration, queued_duration: queued_duration }
- if has_pipeline_badges?(@pipeline)
.well-segment
diff --git a/app/views/projects/pipelines/_pipeline_stats_text.html.haml b/app/views/projects/pipelines/_pipeline_stats_text.html.haml
new file mode 100644
index 00000000000..8adf94e61c4
--- /dev/null
+++ b/app/views/projects/pipelines/_pipeline_stats_text.html.haml
@@ -0,0 +1 @@
+= s_("in %{duration} and was queued for %{queued_duration}").html_safe % { duration: duration, queued_duration: queued_duration }
diff --git a/config/feature_flags/development/disabled_mr_discussions_redis_cache.yml b/config/feature_flags/development/disabled_mr_discussions_redis_cache.yml
deleted file mode 100644
index c4f0ca0effe..00000000000
--- a/config/feature_flags/development/disabled_mr_discussions_redis_cache.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: disabled_mr_discussions_redis_cache
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92752
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368366
-milestone: '15.3'
-type: development
-group: group::code review
-default_enabled: false
diff --git a/config/feature_flags/ops/auto_disabling_web_hooks.yml b/config/feature_flags/ops/auto_disabling_web_hooks.yml
new file mode 100644
index 00000000000..dab5fb7da1a
--- /dev/null
+++ b/config/feature_flags/ops/auto_disabling_web_hooks.yml
@@ -0,0 +1,8 @@
+---
+name: auto_disabling_web_hooks
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113479
+rollout_issue_url:
+milestone: '15.10'
+type: ops
+group: group::integrations
+default_enabled: true
diff --git a/config/metrics/counts_28d/20210216180321_action_monthly_active_users_sfe_edit.yml b/config/metrics/counts_28d/20210216180321_action_monthly_active_users_sfe_edit.yml
index 8dbf5e5ba41..592b02c84bc 100644
--- a/config/metrics/counts_28d/20210216180321_action_monthly_active_users_sfe_edit.yml
+++ b/config/metrics/counts_28d/20210216180321_action_monthly_active_users_sfe_edit.yml
@@ -7,7 +7,9 @@ product_stage: create
product_group: editor
product_category: web_ide
value_type: number
-status: active
+status: removed
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113551
+milestone_removed: 15.10
time_frame: 28d
data_source: redis_hll
distribution:
diff --git a/config/metrics/counts_28d/20210216180323_action_monthly_active_users_snippet_editor_edit.yml b/config/metrics/counts_28d/20210216180323_action_monthly_active_users_snippet_editor_edit.yml
index 5a2d9c197ce..012f9db8c80 100644
--- a/config/metrics/counts_28d/20210216180323_action_monthly_active_users_snippet_editor_edit.yml
+++ b/config/metrics/counts_28d/20210216180323_action_monthly_active_users_snippet_editor_edit.yml
@@ -7,7 +7,9 @@ product_stage: create
product_group: editor
product_category: snippets
value_type: number
-status: active
+status: removed
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113551
+milestone_removed: 15.10
time_frame: 28d
data_source: redis_hll
distribution:
diff --git a/doc/administration/geo/replication/img/adding_a_secondary_v15_8.png b/doc/administration/geo/replication/img/adding_a_secondary_v15_8.png
index 9aad5e889dd..d7c99e6551e 100644
--- a/doc/administration/geo/replication/img/adding_a_secondary_v15_8.png
+++ b/doc/administration/geo/replication/img/adding_a_secondary_v15_8.png
Binary files differ
diff --git a/doc/architecture/blueprints/_template.md b/doc/architecture/blueprints/_template.md
index 2756544e08a..64b79f996e0 100644
--- a/doc/architecture/blueprints/_template.md
+++ b/doc/architecture/blueprints/_template.md
@@ -50,6 +50,7 @@ Blueprint statuses you can use:
- "accepted"
- "ongoing"
- "implemented"
+- "postponed"
- "rejected"
-->
diff --git a/doc/development/advanced_search.md b/doc/development/advanced_search.md
index 4e61098b17d..4c779088d01 100644
--- a/doc/development/advanced_search.md
+++ b/doc/development/advanced_search.md
@@ -42,6 +42,16 @@ After initial indexing is complete, create, update, and delete operations for al
Search queries are generated by the concerns found in [`ee/app/models/concerns/elastic`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/app/models/concerns/elastic). These concerns are also in charge of access control, and have been a historic source of security bugs so please pay close attention to them!
+### Custom routing
+
+[Custom routing](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-routing-field.html#_searching_with_custom_routing)
+is used in Elasticsearch for document types that are associated with a project. The routing format is `project_<project_id>`. Routing is set
+during indexing and searching operations. Some of the benefits and tradeoffs to using custom routing are:
+
+- Project scoped searches are much faster.
+- Routing is not used if too many shards would be hit for global and group scoped searches.
+- Shard size imbalance might occur.
+
## Existing Analyzers/Tokenizers/Filters
These are all defined in [`ee/lib/elastic/latest/config.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/elastic/latest/config.rb)
diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md
index cf10a106119..6a9dd34e703 100644
--- a/doc/development/contributing/index.md
+++ b/doc/development/contributing/index.md
@@ -49,11 +49,11 @@ If you would like to contribute to GitLab:
The general flow of contributing to GitLab is:
-1. [Create a fork](../../user/project/repository/forking_workflow.md#creating-a-fork)
- of GitLab. In some cases, you will want to set up the
+1. [Read about](https://gitlab.com/gitlab-community/meta) and [request access](https://gitlab.com/gitlab-community/meta#request-access-to-community-forks)
+ to the GitLab Community forks. In some cases, you will want to set up the
[GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) to
- [develop against your fork](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#develop-in-your-own-gitlab-fork).
-1. Make your changes in your fork.
+ [develop against the GitLab community fork](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#develop-in-your-own-gitlab-fork).
+1. Create a feature branch and make changes in the [GitLab community fork](https://gitlab.com/gitlab-community/gitlab).
1. When you're ready, [create a new merge request](../../user/project/merge_requests/creating_merge_requests.md).
1. In the merge request's description:
- Ensure you provide complete and accurate information.
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index c9866131cc7..e384b41f6f2 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -80,12 +80,15 @@ intent behind your changes can also help expedite merge request reviews.
To create a merge request:
-1. [Fork](../../user/project/repository/forking_workflow.md) the project into
- your personal namespace (or group) on GitLab.com.
-1. Create a feature branch in your fork (don't work off your [default branch](../../user/project/repository/branches/default.md)).
+1. [Read about](https://gitlab.com/gitlab-community/meta) and [request access](https://gitlab.com/gitlab-community/meta#request-access-to-community-forks)
+ to the GitLab Community forks. In some cases, you will want to set up the
+ [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) to
+ [develop against the GitLab community fork](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#develop-in-your-own-gitlab-fork).
+1. Create a feature branch in the [GitLab community fork](https://gitlab.com/gitlab-community/gitlab)
+ (don't work off the [default branch](../../user/project/repository/branches/default.md)).
1. Follow the [commit messages guidelines](#commit-messages-guidelines).
1. If you have multiple commits, combine them into a few logically organized commits.
-1. Push the commits to your working branch in your fork.
+1. Push the commits to your working branch in the fork.
1. Submit a merge request (MR) against the default branch of the upstream project.
1. The MR title should describe the change you want to make.
1. The MR description should give a reason for your change.
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 95204a6e91c..6d882358e8d 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -6,14 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Vuex
-When there's a clear benefit to separating state management from components (for example, due to state complexity) we recommend using [Vuex](https://vuex.vuejs.org) over any other Flux pattern. Otherwise, feel free to manage state in the components.
-
-Vuex should be strongly considered when:
-
-- You expect multiple parts of the application to react to state changes.
-- There's a need to share data between multiple components.
-- There are complex interactions with Backend, for example, multiple API calls.
-- The app involves interacting with backend via both traditional REST API and GraphQL (especially when moving the REST API over to GraphQL is a pending backend task).
+[Vuex](https://vuex.vuejs.org) should no longer be considered a preferred path to store management and is currently in its legacy phase. This means it is acceptable to add upon existing `Vuex` stores, but we strongly recommend reducing store sizes over time and eventually migrating fully to [Apollo](https://www.apollographql.com/) whenever it's possible. Before adding any new `Vuex` store to an application, first ensure that the `Vue` application you plan to add it into **does not use** `Apollo`. `Vuex` and `Apollo` should not be combined unless absolutely necessary. Please consider reading through [our GraphQL documentation](../fe_guide/graphql.md) for more guidelines on how you can build `Apollo` based applications.
The information included in this page is explained in more detail in the
official [Vuex documentation](https://vuex.vuejs.org).
diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md
index b52a2599ce9..8931f3ef833 100644
--- a/doc/operations/error_tracking.md
+++ b/doc/operations/error_tracking.md
@@ -84,7 +84,7 @@ DSN in **Sentry.io > Project Settings > Client Keys (DSN) > Show deprecated DSN*
ERROR: Sentry failure builds=0 error=raven: dsn missing private key
```
-## Error Tracking List
+## Error Tracking list
Users with at least Reporter [permissions](../user/permissions.md)
can find the Error Tracking list at **Monitor > Error Tracking** in your project's sidebar.
@@ -92,7 +92,7 @@ Here, you can filter errors by title or by status (one of Ignored , Resolved, or
![Error Tracking list](img/error_tracking_list_v12_6.png)
-## Error Details
+## Error details
From error list, users can go to the error details page by selecting the title of any error.
@@ -110,7 +110,7 @@ By default, a **Create issue** button is displayed:
If you create a GitLab issue from the error, the **Create issue** button changes to a **View issue**
button and a link to the GitLab issue displays in the error detail section.
-## Taking Action on errors
+## Taking action on errors
You can take action on Sentry Errors in the GitLab UI. Marking errors as ignored or resolved requires at least Developer role.
@@ -140,12 +140,12 @@ If another event occurs, the error reverts to unresolved.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/7586) in GitLab 15.6.
NOTE:
-Available only on GitLab.com. This feature is currently in [Open Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta).
+Available only on GitLab.com. This feature is in [Open Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta).
### Known limitations
Only basic support is provided with `capture_exception` as the holding method.
-Additional features requests (see this [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340178)) will be added on a case-by-case basis.
+Additional features requests (see this [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340178)) are added on a case-by-case basis.
### Debugging issues
@@ -160,24 +160,18 @@ This example uses the `gitlab.com` instance.
To enable error tracking, follow these steps:
-1. In your project, go to **Settings >Monitor**. Expand the **Error Tracking** tab:
-
- ![MonitorTabPreEnable](img/Monitor_tab-pre-enable.png)
-
-1. Enable Error Tracking with GitLab as backend:
-
- ![MonitorTabPostEnable](img/Monitor_tab-post-enable.png)
-
-1. Select the `Save Changes` button.
-
-1. Copy the DSN string. You will need this later.
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > Monitor**.
+1. Expand the **Error Tracking** tab.
+1. Under **Enable error tracking**, select the **Active** checkbox.
+1. Under **Error tracking backend**, select **GitLab**.
+1. Select **Save changes**.
+1. Copy the DSN string to use later.
### Listing captured errors
Once your application has emitted errors to the Error Tracking API through the Sentry SDK, they should be available under **Monitor > Error Tracking** tab/section.
-![MonitorListErrors](img/Monitor-list_errors.png)
-
For more details, refer to the [GitLab error tracking documentation](https://gitlab.com/help/operations/error_tracking#error-tracking-list).
This process assumes the GDK feature flag `integrated_error_tracking` is enabled. If you are running GDK locally and you do not see the option for error tracking, you can enable it by running the following commands:
@@ -188,7 +182,7 @@ gdk rails console
Feature.enable(:integrated_error_tracking)
```
-### Emitting Errors
+### Emitting errors
#### Supported Sentry types
@@ -206,20 +200,18 @@ Items of various types can be sent to the error tracking app, using either the S
Event item types can contain various interfaces, such as exception, message, stack trace, and template. You can read more about the core data interfaces in [Sentry documentation](https://develop.sentry.dev/sdk/event-payloads/#core-interfaces).
-| Item type | Interface | Can be sent through the Store endpoint | Can be sent through the Envelope endpoint | Currently Supported |
-|---------------|--------------|----------------|----------------|---------------------|
-| event | exception | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
-| event | message | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
-| event | stack trace | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
-| event | template | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
-| transaction | NA | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
-| attachment | NA | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
-| session | NA | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
-| sessions | NA | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
-| user_report | NA | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
-| client_report | NA | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
-
-\* NA = Not Applicable
+| Item type | Interface | Can be sent through the Store endpoint | Can be sent through the Envelope endpoint | Supported |
+| --------------- | ----------- | -------------------------------------- | ----------------------------------------- | ---------------------- |
+| `event` | exception | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
+| `event` | message | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
+| `event` | stack trace | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
+| `event` | template | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
+| `transaction` | N/A | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
+| `attachment` | N/A | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
+| `session` | N/A | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
+| `sessions` | N/A | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
+| `user_report` | N/A | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
+| `client_report` | N/A | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
#### Supported languages
diff --git a/doc/operations/img/Monitor-list_errors.png b/doc/operations/img/Monitor-list_errors.png
deleted file mode 100644
index 06833c08637..00000000000
--- a/doc/operations/img/Monitor-list_errors.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/img/Monitor_tab-post-enable.png b/doc/operations/img/Monitor_tab-post-enable.png
deleted file mode 100644
index 91f0a0d8d27..00000000000
--- a/doc/operations/img/Monitor_tab-post-enable.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/img/Monitor_tab-pre-enable.png b/doc/operations/img/Monitor_tab-pre-enable.png
deleted file mode 100644
index 2984110a7d7..00000000000
--- a/doc/operations/img/Monitor_tab-pre-enable.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.png b/doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.png
index b183013ff7a..fcf7a8352fd 100644
--- a/doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.png
+++ b/doc/user/application_security/policies/img/scheduled_scan_execution_policies_diagram.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.png b/doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.png
index 106b0a13063..c2780fce787 100644
--- a/doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.png
+++ b/doc/user/application_security/security_dashboard/img/security_center_dashboard_v15_10.png
Binary files differ
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 96c20e5d943..0fd4c018bd4 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -522,11 +522,7 @@ The maximum number of direct child epics is 100.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8502) in GitLab 15.6 [with a flag](../../../administration/feature_flags.md) named `child_epics_from_different_hierarchies`. Disabled by default.
> - Minimum required role for the group [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/382503) from Reporter to Guest in GitLab 15.7.
> - Cross-group child epics [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/375622) in GitLab 15.9. Enabled by default.
-
-FLAG:
-On self-managed GitLab, by default this feature is available. To disable it,
-ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `child_epics_from_different_hierarchies`.
-On GitLab.com, this feature is available.
+> - [Feature flag `child_epics_from_different_hierarchies`](https://gitlab.com/gitlab-org/gitlab/-/issues/382719)) removed in GitLab 15.10.
You can add a child epic that belongs to a group that is different from the parent epic's group.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 8e574c4fbd1..355e069a438 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -234,38 +234,24 @@ It can also help to compare the XML response from your provider with our [exampl
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/339888) in GitLab 14.7 to not enforce SSO checks for Git activity originating from CI/CD jobs.
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/215155) in GitLab 15.5 [with a flag](../../../administration/feature_flags.md) named `transparent_sso_enforcement` to include transparent enforcement even when SSO enforcement is not enabled. Disabled on GitLab.com.
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/375788) in GitLab 15.8 by enabling transparent SSO by default on GitLab.com.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/389562) in GitLab 15.10. Feature flag `transparent_sso_enforcement` removed.
-FLAG:
-On self-managed GitLab, transparent SSO enforcement is unavailable. An
-[issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/382917) to add
-transparent SSO enforcement to self-managed GitLab.
-On GitLab.com, transparent SSO enforcement is available by default. To turn off
-transparent SSO, ask a support or production team to enable the
-`transparent_sso_enforcement_override` feature flag for a specific customer
-group.
+On GitLab.com, SSO is enforced:
-#### Transparent SSO enforcement
-
-By default, transparent SSO enforcement is enabled in GitLab.com. This means SSO is enforced:
-
-- When users access groups and projects in the organization's
+- When SAML SSO is enabled.
+- For users with an existing SAML identity when accessing groups and projects in the organization's
group hierarchy. Users can view other groups and projects without SSO sign in.
-- For each user with an existing SAML identity.
-
-When transparent SSO enforcement is enabled, users:
-
-- Are not prompted to sign in through SSO on each visit. GitLab checks
- whether a user has authenticated through SSO. If the user last signed in more
- than 24 hours ago, GitLab prompts the user to sign in again through SSO.
-- Without SAML identities are not required to use SSO unless **Enforce
- SSO-only authentication for web activity for this group** is enabled.
A user has a SAML identity if one or both of the following are true:
- They have signed in to GitLab by using their GitLab group's single sign-on URL.
- They were provisioned by SCIM.
-With transparent SSO enabled, SSO is enforced as follows:
+Users are not prompted to sign in through SSO on each visit. GitLab checks
+whether a user has authenticated through SSO. If the user last signed in more
+than 24 hours ago, GitLab prompts the user to sign in again through SSO.
+
+SSO is enforced as follows:
| Project/Group visibility | Enforce SSO setting | Member with identity | Member without identity | Non-member or not signed in |
|--------------------------|---------------------|--------------------| ------ |------------------------------|
diff --git a/lib/api/entities/internal/pages/lookup_path.rb b/lib/api/entities/internal/pages/lookup_path.rb
index 1bf94f74fb4..1ea41e129b2 100644
--- a/lib/api/entities/internal/pages/lookup_path.rb
+++ b/lib/api/entities/internal/pages/lookup_path.rb
@@ -5,8 +5,12 @@ module API
module Internal
module Pages
class LookupPath < Grape::Entity
- expose :project_id, :access_control,
- :source, :https_only, :prefix
+ expose :access_control,
+ :https_only,
+ :prefix,
+ :project_id,
+ :source,
+ :unique_domain
end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 2e75f09170e..06f3b55b742 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -59,7 +59,7 @@ module API
def cache_client
if Feature.enabled?(:cache_client_with_metrics, user_project)
Gitlab::Cache::Client.build_with_metadata(
- cache_identifier: "#{self.class}#content_sha",
+ cache_identifier: 'API::Files#content_sha',
feature_category: :source_code_management,
backing_resource: :gitaly
)
diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb
index 771059053ac..5664a3df589 100644
--- a/lib/api/internal/pages.rb
+++ b/lib/api/internal/pages.rb
@@ -33,26 +33,7 @@ module API
requires :host, type: String, desc: 'The host to query for'
end
get "/" do
- ##
- # Serverless domain proxy has been deprecated and disabled as per
- # https://gitlab.com/gitlab-org/gitlab-pages/-/issues/467
- #
- # serverless_domain_finder = ServerlessDomainFinder.new(params[:host])
- # if serverless_domain_finder.serverless?
- # # Handle Serverless domains
- # serverless_domain = serverless_domain_finder.execute
- # no_content! unless serverless_domain
- #
- # virtual_domain = Serverless::VirtualDomain.new(serverless_domain)
- # no_content! unless virtual_domain
- #
- # present virtual_domain, with: Entities::Internal::Serverless::VirtualDomain
- # end
-
- host = Namespace.find_by_pages_host(params[:host]) || PagesDomain.find_by_domain_case_insensitive(params[:host])
- no_content! unless host
-
- virtual_domain = host.pages_virtual_domain
+ virtual_domain = ::Gitlab::Pages::VirtualHostFinder.new(params[:host]).execute
no_content! unless virtual_domain
if virtual_domain.cache_key.present?
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index c3a7b384172..82adb0dd369 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -292,23 +292,34 @@ module Gitlab
# order of the ALTER TABLE. This can be useful in situations where the foreign
# key creation could deadlock with another process.
#
- # rubocop: disable Metrics/ParameterLists
- def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, on_update: nil, target_column: :id, name: nil, validate: true, reverse_lock_order: false)
+ def add_concurrent_foreign_key(source, target, column:, **options)
+ options.reverse_merge!({
+ on_delete: :cascade,
+ on_update: nil,
+ target_column: :id,
+ validate: true,
+ reverse_lock_order: false,
+ allow_partitioned: false,
+ column: column
+ })
+
# Transactions would result in ALTER TABLE locks being held for the
# duration of the transaction, defeating the purpose of this method.
if transaction_open?
raise 'add_concurrent_foreign_key can not be run inside a transaction'
end
- options = {
- column: column,
- on_delete: on_delete,
- on_update: on_update,
- name: name.presence || concurrent_foreign_key_name(source, column),
- primary_key: target_column
- }
+ if !options.delete(:allow_partitioned) && table_partitioned?(source)
+ raise ArgumentError, 'add_concurrent_foreign_key can not be used on a partitioned ' \
+ 'table. Please use add_concurrent_partitioned_foreign_key on the partitioned table ' \
+ 'as we need to create foreign keys on each partition and a FK on the parent table'
+ end
- if foreign_key_exists?(source, target, **options)
+ options[:name] ||= concurrent_foreign_key_name(source, column)
+ options[:primary_key] = options[:target_column]
+ check_options = options.slice(:column, :on_delete, :on_update, :name, :primary_key)
+
+ if foreign_key_exists?(source, target, **check_options)
warning_message = "Foreign key not created because it exists already " \
"(this may be due to an aborted migration or similar): " \
"source: #{source}, target: #{target}, column: #{options[:column]}, "\
@@ -321,17 +332,18 @@ module Gitlab
# validating it. This means we keep the ALTER TABLE lock only for a
# short period of time. The key _is_ enforced for any newly created
# data.
+ not_valid_option = 'NOT VALID' unless table_partitioned?(source)
with_lock_retries do
- execute("LOCK TABLE #{target}, #{source} IN SHARE ROW EXCLUSIVE MODE") if reverse_lock_order
+ execute("LOCK TABLE #{target}, #{source} IN SHARE ROW EXCLUSIVE MODE") if options[:reverse_lock_order]
execute <<-EOF.strip_heredoc
ALTER TABLE #{source}
ADD CONSTRAINT #{options[:name]}
FOREIGN KEY (#{multiple_columns(options[:column])})
- REFERENCES #{target} (#{multiple_columns(target_column)})
+ REFERENCES #{target} (#{multiple_columns(options[:target_column])})
#{on_update_statement(options[:on_update])}
#{on_delete_statement(options[:on_delete])}
- NOT VALID;
+ #{not_valid_option};
EOF
end
end
@@ -345,13 +357,12 @@ module Gitlab
#
# Note this is a no-op in case the constraint is VALID already
- if validate
+ if options[:validate]
disable_statement_timeout do
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{options[:name]};")
end
end
end
- # rubocop: enable Metrics/ParameterLists
def validate_foreign_key(source, column, name: nil)
fk_name = name || concurrent_foreign_key_name(source, column)
@@ -1239,6 +1250,12 @@ into similar problems in the future (e.g. when new tables are created).
end
end
+ def table_partitioned?(table_name)
+ Gitlab::Database::PostgresPartitionedTable
+ .find_by_name_in_current_schema(table_name)
+ .present?
+ end
+
private
def multiple_columns(columns, separator: ', ')
diff --git a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
index 8849191f356..7d9c12d776e 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
@@ -32,46 +32,75 @@ module Gitlab
# column - The name of the column to create the foreign key on.
# on_delete - The action to perform when associated data is removed,
# defaults to "CASCADE".
+ # on_update - The action to perform when associated data is updated,
+ # no default value is set.
# name - The name of the foreign key.
+ # validate - Flag that controls whether the new foreign key will be
+ # validated after creation and if it will be added on the parent table.
+ # If the flag is not set, the constraint will only be enforced for new data
+ # in the existing partitions. The helper will need to be called again
+ # with the flag set to `true` to add the foreign key on the parent table
+ # after validating it on all partitions.
+ # `validate: false` should be paired with `prepare_partitioned_async_foreign_key_validation`
+ # reverse_lock_order - Flag that controls whether we should attempt to acquire locks in the reverse
+ # order of the ALTER TABLE. This can be useful in situations where the foreign
+ # key creation could deadlock with another process.
#
- def add_concurrent_partitioned_foreign_key(source, target, column:, on_delete: :cascade, name: nil)
+ def add_concurrent_partitioned_foreign_key(source, target, column:, **options)
assert_not_in_transaction_block(scope: ERROR_SCOPE)
- partition_options = {
- column: column,
- on_delete: on_delete,
+ options.reverse_merge!({
+ target_column: :id,
+ on_delete: :cascade,
+ on_update: nil,
+ name: nil,
+ validate: true,
+ reverse_lock_order: false,
+ column: column
+ })
- # We'll use the same FK name for all partitions and match it to
- # the name used for the partitioned table to follow the convention
- # used by PostgreSQL when adding FKs to new partitions
- name: name.presence || concurrent_partitioned_foreign_key_name(source, column),
+ # We'll use the same FK name for all partitions and match it to
+ # the name used for the partitioned table to follow the convention
+ # used by PostgreSQL when adding FKs to new partitions
+ options[:name] ||= concurrent_partitioned_foreign_key_name(source, column)
+ check_options = options.slice(:column, :on_delete, :on_update, :name)
+ check_options[:primary_key] = options[:target_column]
- # Force the FK validation to true for partitions (and the partitioned table)
- validate: true
- }
-
- if foreign_key_exists?(source, target, **partition_options)
+ if foreign_key_exists?(source, target, **check_options)
warning_message = "Foreign key not created because it exists already " \
"(this may be due to an aborted migration or similar): " \
- "source: #{source}, target: #{target}, column: #{partition_options[:column]}, "\
- "name: #{partition_options[:name]}, on_delete: #{partition_options[:on_delete]}"
+ "source: #{source}, target: #{target}, column: #{options[:column]}, "\
+ "name: #{options[:name]}, on_delete: #{options[:on_delete]}, "\
+ "on_update: #{options[:on_update]}"
Gitlab::AppLogger.warn warning_message
return
end
- partitioned_table = find_partitioned_table(source)
-
- partitioned_table.postgres_partitions.order(:name).each do |partition|
- add_concurrent_foreign_key(partition.identifier, target, **partition_options)
+ Gitlab::Database::PostgresPartitionedTable.each_partition(source) do |partition|
+ add_concurrent_foreign_key(partition.identifier, target, **options)
end
- with_lock_retries do
- add_foreign_key(source, target, **partition_options)
+ # If we are to add the FK on the parent table now, it will trigger
+ # the validation on all partitions. The helper must be called one
+ # more time with `validate: true` after the FK is valid on all partitions.
+ return unless options[:validate]
+
+ options[:allow_partitioned] = true
+ add_concurrent_foreign_key(source, target, **options)
+ end
+
+ def validate_partitioned_foreign_key(source, column, name: nil)
+ assert_not_in_transaction_block(scope: ERROR_SCOPE)
+
+ Gitlab::Database::PostgresPartitionedTable.each_partition(source) do |partition|
+ validate_foreign_key(partition.identifier, column, name: name)
end
end
+ private
+
# Returns the name for a concurrent partitioned foreign key.
#
# Similar to concurrent_foreign_key_name (Gitlab::Database::MigrationHelpers)
diff --git a/lib/gitlab/pages/virtual_host_finder.rb b/lib/gitlab/pages/virtual_host_finder.rb
new file mode 100644
index 00000000000..87fbf547770
--- /dev/null
+++ b/lib/gitlab/pages/virtual_host_finder.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pages
+ class VirtualHostFinder
+ def initialize(host)
+ @host = host&.downcase
+ end
+
+ def execute
+ return if host.blank?
+
+ gitlab_host = ::Settings.pages.host.downcase.prepend(".")
+
+ if host.ends_with?(gitlab_host)
+ name = host.delete_suffix(gitlab_host)
+
+ by_namespace_domain(name) ||
+ by_unique_domain(name)
+ else
+ by_custom_domain(host)
+ end
+ end
+
+ private
+
+ attr_reader :host
+
+ def by_unique_domain(name)
+ return unless Feature.enabled?(:pages_unique_domain)
+
+ project = Project.by_pages_enabled_unique_domain(name)
+
+ return unless project&.pages_deployed?
+
+ ::Pages::VirtualDomain.new(projects: [project])
+ end
+
+ def by_namespace_domain(name)
+ namespace = Namespace.top_most.by_path(name)
+
+ return if namespace.blank?
+
+ cache = if Feature.enabled?(:cache_pages_domain_api, namespace)
+ ::Gitlab::Pages::CacheControl.for_namespace(namespace.id)
+ end
+
+ ::Pages::VirtualDomain.new(
+ trim_prefix: namespace.full_path,
+ projects: namespace.all_projects_with_pages,
+ cache: cache
+ )
+ end
+
+ def by_custom_domain(host)
+ domain = PagesDomain.find_by_domain_case_insensitive(host)
+
+ return unless domain&.pages_deployed?
+
+ cache = if Feature.enabled?(:cache_pages_domain_api, domain.project.root_namespace)
+ ::Gitlab::Pages::CacheControl.for_domain(domain.id)
+ end
+
+ ::Pages::VirtualDomain.new(
+ projects: [domain.project],
+ domain: domain,
+ cache: cache
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 20aafa277f1..52b8d70c113 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -448,10 +448,7 @@ module Gitlab
remote_mirrors: distinct_count(::Project.with_remote_mirrors.where(time_period), :creator_id),
snippets: distinct_count(::Snippet.where(time_period), :author_id)
}.tap do |h|
- if time_period.present?
- h[:merge_requests_users] = merge_requests_users(time_period)
- h.merge!(action_monthly_active_users(time_period))
- end
+ h[:merge_requests_users] = merge_requests_users(time_period) if time_period.present?
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -565,16 +562,6 @@ module Gitlab
{}
end
- def action_monthly_active_users(time_period)
- counter = Gitlab::UsageDataCounters::EditorUniqueCounter
- date_range = { date_from: time_period[:created_at].first, date_to: time_period[:created_at].last }
-
- {
- action_monthly_active_users_sfe_edit: redis_usage_data { counter.count_sfe_edit_actions(**date_range) },
- action_monthly_active_users_snippet_editor_edit: redis_usage_data { counter.count_snippet_editor_edit_actions(**date_range) }
- }
- end
-
def with_metadata
result = nil
error = nil
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index baf806c2e68..544bbd52c6b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -31710,12 +31710,6 @@ msgstr ""
msgid "Pipelines|\"Hello world\" with GitLab CI"
msgstr ""
-msgid "Pipelines|%{jobs} %{ref_text} in %{duration}"
-msgstr ""
-
-msgid "Pipelines|(queued for %{queued_duration})"
-msgstr ""
-
msgid "Pipelines|1. Set up a runner"
msgstr ""
@@ -44331,9 +44325,6 @@ msgstr ""
msgid "This epic cannot be added. An epic cannot be added to itself."
msgstr ""
-msgid "This epic cannot be added. An epic must belong to the same group or subgroup as its parent epic."
-msgstr ""
-
msgid "This epic cannot be added. It is already an ancestor of the parent epic."
msgstr ""
@@ -51538,6 +51529,12 @@ msgstr ""
msgid "in"
msgstr ""
+msgid "in %{duration} and was queued for %{queued_duration}"
+msgstr ""
+
+msgid "in %{duration}, using %{compute_credits} compute credits, and was queued for %{queued_duration}"
+msgstr ""
+
msgid "in group %{link_to_group}"
msgstr ""
diff --git a/package.json b/package.json
index 5de2be50c15..4431bcde0ca 100644
--- a/package.json
+++ b/package.json
@@ -225,7 +225,7 @@
"cheerio": "^1.0.0-rc.9",
"commander": "^2.20.3",
"custom-jquery-matchers": "^2.1.0",
- "eslint": "8.35.0",
+ "eslint": "8.36.0",
"eslint-import-resolver-jest": "3.0.2",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-import": "^2.27.5",
diff --git a/rubocop/rubocop-ruby30.yml b/rubocop/rubocop-ruby30.yml
index d46bb9388a3..b984d761b80 100644
--- a/rubocop/rubocop-ruby30.yml
+++ b/rubocop/rubocop-ruby30.yml
@@ -2,8 +2,8 @@
# Ruby 3.0.
#
# After the transition has been completed:
-# * Move all configuration which enable cops to .rubocop.yml.
-# * Remove all reminaing configuration.
+# * Move all configuration which enabled or tweaked cops to .rubocop.yml.
+# * Remove all remaining configuration.
# These cops are disabled in Ruby 2.7 (rubocop-27.yml).
Style/MutableConstant:
diff --git a/rubocop/rubocop-ruby31.yml b/rubocop/rubocop-ruby31.yml
new file mode 100644
index 00000000000..109c7ca2dfe
--- /dev/null
+++ b/rubocop/rubocop-ruby31.yml
@@ -0,0 +1,10 @@
+# RuboCop configuration adjustments during the transition time from Ruby 3.0 to
+# Ruby 3.1.
+#
+# After the transition has been completed:
+# * Move all configuration which enabled or tweaked cops to .rubocop.yml.
+# * Remove all remaining configuration.
+
+# Short-hand Hash syntax does not work prior 3.1.
+Style/HashSyntax:
+ EnforcedShorthandSyntax: never
diff --git a/scripts/lint-docs-blueprints.rb b/scripts/lint-docs-blueprints.rb
index d0fede76c84..d0a0a6a05de 100755
--- a/scripts/lint-docs-blueprints.rb
+++ b/scripts/lint-docs-blueprints.rb
@@ -22,7 +22,7 @@ def extract_front_matter(path)
end
class BlueprintFrontMatter
- STATUSES = %w[proposed accepted ongoing implemented rejected]
+ STATUSES = %w[proposed accepted ongoing implemented postponed rejected]
attr_reader :errors
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 55005d0abff..df8a5825dab 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -1,10 +1,19 @@
function retry() {
+ retry_times_sleep 2 3 "$@"
+}
+
+function retry_times_sleep() {
+ number_of_retries="$1"
+ shift
+ sleep_seconds="$1"
+ shift
+
if eval "$@"; then
return 0
fi
- for i in 2 1; do
- sleep 3s
+ for i in $(seq "${number_of_retries}" -1 1); do
+ sleep "$sleep_seconds"s
echo "[$(date '+%H:%M:%S')] Retrying $i..."
if eval "$@"; then
return 0
@@ -32,6 +41,7 @@ function retry_exponential() {
return 0
fi
done
+
return 1
}
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 5185aa64d9f..3476c7b8465 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe Oauth::AuthorizationsController do
let(:application_scopes) { 'api read_user' }
let!(:application) do
- create(:oauth_application, scopes: application_scopes,
- redirect_uri: 'http://example.com')
+ create(:oauth_application, scopes: application_scopes, redirect_uri: 'http://example.com')
end
let(:params) do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 1f78314c372..70876f137c3 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -334,9 +334,10 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category:
expect(controller).to receive(:atlassian_oauth2).and_wrap_original do |m, *args|
m.call(*args)
- expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2')
+ expect(Gitlab::ApplicationContext.current).to include(
+ 'meta.user' => user.username,
+ 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2'
+ )
end
post :atlassian_oauth2
@@ -441,8 +442,12 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category:
before do
stub_last_request_id(last_request_id)
- stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
- providers: [saml_config])
+ stub_omniauth_saml_config(
+ enabled: true,
+ auto_link_saml_user: true,
+ allow_single_sign_on: ['saml'],
+ providers: [saml_config]
+ )
mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
request.env['devise.mapping'] = Devise.mappings[:user]
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
@@ -533,9 +538,10 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category:
expect(controller).to receive(:saml).and_wrap_original do |m, *args|
m.call(*args)
- expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => user.username,
- 'meta.caller_id' => 'OmniauthCallbacksController#saml')
+ expect(Gitlab::ApplicationContext.current).to include(
+ 'meta.user' => user.username,
+ 'meta.caller_id' => 'OmniauthCallbacksController#saml'
+ )
end
post :saml, params: { SAMLResponse: mock_saml_response }
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 343c7f53022..098d1201939 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -113,6 +113,50 @@ RSpec.describe 'Pipeline', :js, feature_category: :projects do
end
end
+ describe 'pipeline stats text' do
+ let(:finished_pipeline) do
+ create(:ci_pipeline, :success, project: project,
+ ref: 'master', sha: project.commit.id, user: user)
+ end
+
+ before do
+ finished_pipeline.update!(started_at: "2023-01-01 01:01:05", created_at: "2023-01-01 01:01:01",
+ finished_at: "2023-01-01 01:01:10", duration: 9)
+ end
+
+ context 'pipeline has finished' do
+ it 'shows pipeline stats with flag on' do
+ visit project_pipeline_path(project, finished_pipeline)
+
+ within '.pipeline-info' do
+ expect(page).to have_content("in #{finished_pipeline.duration} seconds")
+ expect(page).to have_content("and was queued for #{finished_pipeline.queued_duration} seconds")
+ end
+ end
+
+ it 'shows pipeline stats with flag off' do
+ stub_feature_flags(refactor_ci_minutes_consumption: false)
+
+ visit project_pipeline_path(project, finished_pipeline)
+
+ within '.pipeline-info' do
+ expect(page).to have_content("in #{finished_pipeline.duration} seconds " \
+ "and was queued for #{finished_pipeline.queued_duration} seconds")
+ end
+ end
+ end
+
+ context 'pipeline has not finished' do
+ it 'does not show pipeline stats' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_selector('[data-testid="pipeline-stats-text"]')
+ end
+ end
+ end
+ end
+
describe 'related merge requests' do
context 'when there are no related merge requests' do
it 'shows a "no related merge requests" message' do
diff --git a/spec/fixtures/api/schemas/internal/pages/lookup_path.json b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
index 9d81ea495f1..8ca71870911 100644
--- a/spec/fixtures/api/schemas/internal/pages/lookup_path.json
+++ b/spec/fixtures/api/schemas/internal/pages/lookup_path.json
@@ -23,7 +23,8 @@
},
"additionalProperties": false
},
- "prefix": { "type": "string" }
+ "prefix": { "type": "string" },
+ "unique_domain": { "type": ["string", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
index 8e4b60efa67..5f0847e0db6 100644
--- a/spec/frontend/analytics/shared/components/daterange_spec.js
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -86,18 +86,19 @@ describe('Daterange component', () => {
});
describe('set', () => {
- it('emits the change event with an object containing startDate and endDate', () => {
+ it('emits the change event with an object containing startDate and endDate', async () => {
const startDate = new Date('2019-10-01');
const endDate = new Date('2019-10-05');
- wrapper.vm.dateRange = { startDate, endDate };
- expect(wrapper.emitted().change).toEqual([[{ startDate, endDate }]]);
+ await findDaterangePicker().vm.$emit('input', { startDate, endDate });
+
+ expect(wrapper.emitted('change')).toEqual([[{ startDate, endDate }]]);
});
});
describe('get', () => {
- it("returns value of dateRange from state's startDate and endDate", () => {
- expect(wrapper.vm.dateRange).toEqual({
+ it("datepicker to have default of dateRange from state's startDate and endDate", () => {
+ expect(findDaterangePicker().props('value')).toEqual({
startDate: defaultProps.startDate,
endDate: defaultProps.endDate,
});
diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js
deleted file mode 100644
index 86c4ad9b361..00000000000
--- a/spec/frontend/work_items/components/notes/activity_filter_spec.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import {
- WORK_ITEM_NOTES_FILTER_ALL_NOTES,
- WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
- WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
- TRACKING_CATEGORY_SHOW,
-} from '~/work_items/constants';
-
-import { mockTracking } from 'helpers/tracking_helper';
-
-describe('Work Item Activity/Discussions Filtering', () => {
- let wrapper;
-
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findOnlyCommentsItem = () => wrapper.findByTestId('comments-activity');
- const findOnlyHistoryItem = () => wrapper.findByTestId('history-activity');
-
- const createComponent = ({
- discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
- loading = false,
- workItemType = 'Task',
- } = {}) => {
- wrapper = shallowMountExtended(ActivityFilter, {
- propsData: {
- discussionFilter,
- loading,
- workItemType,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- describe('Default', () => {
- it('has a dropdown with 3 options', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findAllDropdownItems()).toHaveLength(ActivityFilter.filterOptions.length);
- });
-
- it('has local storage sync with the correct props', () => {
- expect(findLocalStorageSync().props('asString')).toBe(true);
- });
-
- it('emits `changeFilter` event when local storage input is emitted', () => {
- findLocalStorageSync().vm.$emit('input', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
-
- expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]);
- });
- });
-
- describe('Changing filter value', () => {
- it.each`
- dropdownLabel | filterValue | dropdownItem
- ${'Comments only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${findOnlyCommentsItem}
- ${'History only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_HISTORY} | ${findOnlyHistoryItem}
- `(
- 'when `$dropdownLabel` is clicked it emits `$filterValue` with tracking info',
- ({ dropdownItem, filterValue }) => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- dropdownItem().vm.$emit('click');
-
- expect(wrapper.emitted('changeFilter')).toEqual([[filterValue]]);
-
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'work_item_notes_filter_changed',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_filtering',
- property: 'type_Task',
- },
- );
- },
- );
- });
-});
diff --git a/spec/frontend/work_items/components/notes/activity_sort_spec.js b/spec/frontend/work_items/components/notes/activity_sort_spec.js
deleted file mode 100644
index 289823dc59e..00000000000
--- a/spec/frontend/work_items/components/notes/activity_sort_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ActivitySort from '~/work_items/components/notes/activity_sort.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { ASC, DESC } from '~/notes/constants';
-
-import { mockTracking } from 'helpers/tracking_helper';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-
-describe('Work Item Activity Sorting', () => {
- let wrapper;
-
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findNewestFirstItem = () => wrapper.findByTestId('newest-first');
-
- const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => {
- wrapper = shallowMountExtended(ActivitySort, {
- propsData: {
- sortOrder,
- loading,
- workItemType,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- describe('default', () => {
- it('has a dropdown with 2 options', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findAllDropdownItems()).toHaveLength(ActivitySort.sortOptions.length);
- });
-
- it('has local storage sync with the correct props', () => {
- expect(findLocalStorageSync().props('asString')).toBe(true);
- });
-
- it('emits `changeSort` event when update is emitted', () => {
- findLocalStorageSync().vm.$emit('input', ASC);
-
- expect(wrapper.emitted('changeSort')).toEqual([[ASC]]);
- });
- });
-
- describe('when asc', () => {
- describe('when the dropdown is clicked', () => {
- it('calls the right actions', () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- findNewestFirstItem().vm.$emit('click');
-
- expect(wrapper.emitted('changeSort')).toEqual([[DESC]]);
-
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'work_item_notes_sort_order_changed',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_sorting',
- property: 'type_Task',
- },
- );
- });
- });
- });
-});
diff --git a/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
new file mode 100644
index 00000000000..5ed9d581446
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
@@ -0,0 +1,109 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemActivitySortFilter from '~/work_items/components/notes/work_item_activity_sort_filter.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { ASC, DESC } from '~/notes/constants';
+import {
+ WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ WORK_ITEM_NOTES_FILTER_KEY,
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_ACTIVITY_FILTER_OPTIONS,
+ TRACKING_CATEGORY_SHOW,
+} from '~/work_items/constants';
+
+import { mockTracking } from 'helpers/tracking_helper';
+
+describe('Work Item Activity/Discussions Filtering', () => {
+ let wrapper;
+
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findByDataTestId = (dataTestId) => wrapper.findByTestId(dataTestId);
+
+ const createComponent = ({
+ loading = false,
+ workItemType = 'Task',
+ sortFilterProp = ASC,
+ filterOptions = WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ trackingLabel = 'item_track_notes_sorting',
+ trackingAction = 'work_item_notes_sort_order_changed',
+ filterEvent = 'changeSort',
+ defaultSortFilterProp = ASC,
+ storageKey = WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemActivitySortFilter, {
+ propsData: {
+ loading,
+ workItemType,
+ sortFilterProp,
+ filterOptions,
+ trackingLabel,
+ trackingAction,
+ filterEvent,
+ defaultSortFilterProp,
+ storageKey,
+ },
+ });
+ };
+
+ describe.each`
+ usedFor | filterOptions | storageKey | filterEvent | newInputOption | trackingLabel | trackingAction | defaultSortFilterProp | sortFilterProp | nonDefaultDataTestId
+ ${'Sorting'} | ${WORK_ITEM_ACTIVITY_SORT_OPTIONS} | ${WORK_ITEM_NOTES_SORT_ORDER_KEY} | ${'changeSort'} | ${DESC} | ${'item_track_notes_sorting'} | ${'work_item_notes_sort_order_changed'} | ${ASC} | ${ASC} | ${'newest-first'}
+ ${'Filtering'} | ${WORK_ITEM_ACTIVITY_FILTER_OPTIONS} | ${WORK_ITEM_NOTES_FILTER_KEY} | ${'changeFilter'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${'item_track_notes_sorting'} | ${'work_item_notes_filter_changed'} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${'comments-activity'}
+ `(
+ 'When used for $usedFor',
+ ({
+ filterOptions,
+ storageKey,
+ filterEvent,
+ trackingLabel,
+ trackingAction,
+ newInputOption,
+ defaultSortFilterProp,
+ sortFilterProp,
+ nonDefaultDataTestId,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ sortFilterProp,
+ filterOptions,
+ trackingLabel,
+ trackingAction,
+ filterEvent,
+ defaultSortFilterProp,
+ storageKey,
+ });
+ });
+
+ it('has a dropdown with options equal to the length of `filterOptions`', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findAllDropdownItems()).toHaveLength(filterOptions.length);
+ });
+
+ it('has local storage sync with the correct props', () => {
+ expect(findLocalStorageSync().props('asString')).toBe(true);
+ expect(findLocalStorageSync().props('storageKey')).toBe(storageKey);
+ });
+
+ it(`emits ${filterEvent} event when local storage input is emitted`, () => {
+ findLocalStorageSync().vm.$emit('input', newInputOption);
+
+ expect(wrapper.emitted(filterEvent)).toEqual([[newInputOption]]);
+ });
+
+ it('emits tracking event when the a non default dropdown item is clicked', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ findByDataTestId(nonDefaultDataTestId).vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, trackingAction, {
+ category: TRACKING_CATEGORY_SHOW,
+ label: trackingLabel,
+ property: 'type_Task',
+ });
+ });
+ },
+ );
+});
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 3b87a5e3e88..daf74f7a93b 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
@@ -1,7 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
-import ActivitySort from '~/work_items/components/notes/activity_sort.vue';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
import { ASC } from '~/notes/constants';
import {
WORK_ITEM_NOTES_FILTER_ALL_NOTES,
@@ -12,8 +10,8 @@ describe('Work Item Note Activity Header', () => {
let wrapper;
const findActivityLabelHeading = () => wrapper.find('h3');
- const findActivityFilterDropdown = () => wrapper.findComponent(ActivityFilter);
- const findActivitySortDropdown = () => wrapper.findComponent(ActivitySort);
+ const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter');
+ const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort');
const createComponent = ({
disableActivityFilterSort = false,
@@ -21,7 +19,7 @@ describe('Work Item Note Activity Header', () => {
workItemType = 'Task',
discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
} = {}) => {
- wrapper = shallowMount(WorkItemNotesActivityHeader, {
+ wrapper = shallowMountExtended(WorkItemNotesActivityHeader, {
propsData: {
disableActivityFilterSort,
sortOrder,
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 63a470c82a1..1af69f85a54 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::MigrationHelpers do
+RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database do
include Database::TableSchemaHelpers
include Database::TriggerHelpers
@@ -979,6 +979,65 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ context 'when creating foreign key on a partitioned table' do
+ before do
+ model.execute(<<~SQL)
+ CREATE TABLE public._test_source_partitioned_table (
+ id serial NOT NULL,
+ partition_id serial NOT NULL,
+ owner_id bigint NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ ) PARTITION BY LIST(partition_id);
+
+ CREATE TABLE _test_source_partitioned_table_1
+ PARTITION OF public._test_source_partitioned_table
+ FOR VALUES IN (1);
+
+ CREATE TABLE public._test_dest_partitioned_table (
+ id serial NOT NULL,
+ partition_id serial NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ );
+ SQL
+ end
+
+ it 'creates the FK without using NOT VALID', :aggregate_failures do
+ allow(model).to receive(:execute).and_call_original
+
+ expect(model).to receive(:with_lock_retries).and_yield
+
+ expect(model).to receive(:execute).with(
+ "ALTER TABLE _test_source_partitioned_table\n" \
+ "ADD CONSTRAINT fk_multiple_columns\n" \
+ "FOREIGN KEY \(partition_id, owner_id\)\n" \
+ "REFERENCES _test_dest_partitioned_table \(partition_id, id\)\n" \
+ "ON UPDATE CASCADE\n" \
+ "ON DELETE CASCADE\n;\n"
+ )
+
+ model.add_concurrent_foreign_key(
+ :_test_source_partitioned_table,
+ :_test_dest_partitioned_table,
+ column: [:partition_id, :owner_id],
+ target_column: [:partition_id, :id],
+ name: :fk_multiple_columns,
+ on_update: :cascade,
+ allow_partitioned: true
+ )
+ end
+
+ it 'raises an error if allow_partitioned is not set' do
+ expect(model).not_to receive(:with_lock_retries).and_yield
+ expect(model).not_to receive(:execute).with(/FOREIGN KEY/)
+
+ args = [:_test_source_partitioned_table, :_test_dest_partitioned_table]
+ options = { column: [:partition_id, :owner_id], target_column: [:partition_id, :id] }
+
+ expect { model.add_concurrent_foreign_key(*args, **options) }
+ .to raise_error ArgumentError, /use add_concurrent_partitioned_foreign_key/
+ end
+ end
end
end
@@ -2896,4 +2955,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
it { is_expected.to be_falsey }
end
end
+
+ describe "#table_partitioned?" do
+ subject { model.table_partitioned?(table_name) }
+
+ let(:table_name) { 'p_ci_builds_metadata' }
+
+ it { is_expected.to be_truthy }
+
+ context 'with a non-partitioned table' do
+ let(:table_name) { 'users' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
index f0e34476cf2..d5f4afd7ba4 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers do
+RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers, feature_category: :database do
include Database::TableSchemaHelpers
let(:migration) do
@@ -16,15 +16,23 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
let(:partition_schema) { 'gitlab_partitions_dynamic' }
let(:partition1_name) { "#{partition_schema}.#{source_table_name}_202001" }
let(:partition2_name) { "#{partition_schema}.#{source_table_name}_202002" }
+ let(:validate) { true }
let(:options) do
{
column: column_name,
name: foreign_key_name,
on_delete: :cascade,
- validate: true
+ on_update: nil,
+ primary_key: :id
}
end
+ let(:create_options) do
+ options
+ .except(:primary_key)
+ .merge!(reverse_lock_order: false, target_column: :id, validate: validate)
+ end
+
before do
allow(migration).to receive(:puts)
allow(migration).to receive(:transaction_open?).and_return(false)
@@ -67,12 +75,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name)
- expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **options)
- expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **options)
+ expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **create_options)
+ expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **create_options)
- expect(migration).to receive(:with_lock_retries).ordered.and_yield
- expect(migration).to receive(:add_foreign_key)
- .with(source_table_name, target_table_name, **options)
+ expect(migration).to receive(:add_concurrent_foreign_key)
+ .with(source_table_name, target_table_name, allow_partitioned: true, **create_options)
.ordered
.and_call_original
@@ -81,6 +88,39 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
expect_foreign_key_to_exist(source_table_name, foreign_key_name)
end
+ context 'with validate: false option' do
+ let(:validate) { false }
+ let(:options) do
+ {
+ column: column_name,
+ name: foreign_key_name,
+ on_delete: :cascade,
+ on_update: nil,
+ primary_key: :id
+ }
+ end
+
+ it 'creates the foreign key only on partitions' do
+ expect(migration).to receive(:foreign_key_exists?)
+ .with(source_table_name, target_table_name, **options)
+ .and_return(false)
+
+ expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name)
+
+ expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **create_options)
+ expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **create_options)
+
+ expect(migration).not_to receive(:add_concurrent_foreign_key)
+ .with(source_table_name, target_table_name, **create_options)
+
+ migration.add_concurrent_partitioned_foreign_key(
+ source_table_name, target_table_name,
+ column: column_name, validate: false)
+
+ expect_foreign_key_not_to_exist(source_table_name, foreign_key_name)
+ end
+ end
+
def expect_add_concurrent_fk_and_call_original(source_table_name, target_table_name, options)
expect(migration).to receive(:add_concurrent_foreign_key)
.ordered
@@ -100,8 +140,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
.and_return(true)
expect(migration).not_to receive(:add_concurrent_foreign_key)
- expect(migration).not_to receive(:with_lock_retries)
- expect(migration).not_to receive(:add_foreign_key)
migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name, column: column_name)
@@ -110,30 +148,43 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
end
context 'when additional foreign key options are given' do
- let(:options) do
+ let(:exits_options) do
{
column: column_name,
name: '_my_fk_name',
on_delete: :restrict,
- validate: true
+ on_update: nil,
+ primary_key: :id
}
end
+ let(:create_options) do
+ exits_options
+ .except(:primary_key)
+ .merge!(reverse_lock_order: false, target_column: :id, validate: true)
+ end
+
it 'forwards them to the foreign key helper methods' do
expect(migration).to receive(:foreign_key_exists?)
- .with(source_table_name, target_table_name, **options)
+ .with(source_table_name, target_table_name, **exits_options)
.and_return(false)
expect(migration).not_to receive(:concurrent_partitioned_foreign_key_name)
- expect_add_concurrent_fk(partition1_name, target_table_name, **options)
- expect_add_concurrent_fk(partition2_name, target_table_name, **options)
+ expect_add_concurrent_fk(partition1_name, target_table_name, **create_options)
+ expect_add_concurrent_fk(partition2_name, target_table_name, **create_options)
- expect(migration).to receive(:with_lock_retries).ordered.and_yield
- expect(migration).to receive(:add_foreign_key).with(source_table_name, target_table_name, **options).ordered
+ expect(migration).to receive(:add_concurrent_foreign_key)
+ .with(source_table_name, target_table_name, allow_partitioned: true, **create_options)
+ .ordered
- migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name,
- column: column_name, name: '_my_fk_name', on_delete: :restrict)
+ migration.add_concurrent_partitioned_foreign_key(
+ source_table_name,
+ target_table_name,
+ column: column_name,
+ name: '_my_fk_name',
+ on_delete: :restrict
+ )
end
def expect_add_concurrent_fk(source_table_name, target_table_name, options)
@@ -153,4 +204,39 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers
end
end
end
+
+ describe '#validate_partitioned_foreign_key' do
+ context 'when run inside a transaction block' do
+ it 'raises an error' do
+ expect(migration).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ migration.validate_partitioned_foreign_key(source_table_name, column_name, name: '_my_fk_name')
+ end.to raise_error(/can not be run inside a transaction/)
+ end
+ end
+
+ context 'when run outside a transaction block' do
+ before do
+ migration.add_concurrent_partitioned_foreign_key(
+ source_table_name,
+ target_table_name,
+ column: column_name,
+ name: foreign_key_name,
+ validate: false
+ )
+ end
+
+ it 'validates FK for each partition' do
+ expect(migration).to receive(:execute).with(/SET statement_timeout TO 0/).twice
+ expect(migration).to receive(:execute).with(/RESET statement_timeout/).twice
+ expect(migration).to receive(:execute)
+ .with(/ALTER TABLE #{partition1_name} VALIDATE CONSTRAINT #{foreign_key_name}/).ordered
+ expect(migration).to receive(:execute)
+ .with(/ALTER TABLE #{partition2_name} VALIDATE CONSTRAINT #{foreign_key_name}/).ordered
+
+ migration.validate_partitioned_foreign_key(source_table_name, column_name, name: foreign_key_name)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
new file mode 100644
index 00000000000..4b584a45503
--- /dev/null
+++ b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.update_pages_deployment!(create(:pages_deployment, project: project))
+ end
+
+ it 'returns nil when host is empty' do
+ expect(described_class.new(nil).execute).to be_nil
+ expect(described_class.new('').execute).to be_nil
+ end
+
+ context 'when host is a pages custom domain host' do
+ let_it_be(:pages_domain) { create(:pages_domain, project: project) }
+
+ subject(:virtual_domain) { described_class.new(pages_domain.domain).execute }
+
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_domain_#{pages_domain.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to be_nil
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+ end
+ end
+ end
+
+ context 'when host is a namespace domain' do
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns no result if the provided host is not subdomain of the Pages host' do
+ virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute
+
+ expect(virtual_domain).to eq(nil)
+ end
+
+ it 'returns the virual domain with no lookup_paths' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(0)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain with no lookup_paths' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}".downcase).execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to be_nil
+ expect(virtual_domain.lookup_paths.length).to eq(0)
+ end
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ project.namespace.update!(path: 'topNAMEspace')
+ end
+
+ it 'returns no result if the provided host is not subdomain of the Pages host' do
+ virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute
+
+ expect(virtual_domain).to eq(nil)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ it 'finds domain with case-insensitive' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host.upcase}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before_all do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute
+
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.cache_key).to be_nil
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+ end
+ end
+ end
+
+ context 'when host is a unique domain' do
+ before_all do
+ project.project_setting.update!(pages_unique_domain: 'unique-domain')
+ end
+
+ subject(:virtual_domain) { described_class.new("unique-domain.#{Settings.pages.host.upcase}").execute }
+
+ context 'when pages unique domain is enabled' do
+ before_all do
+ project.project_setting.update!(pages_unique_domain_enabled: true)
+ end
+
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+
+ context 'when :cache_pages_domain_api is disabled' do
+ before do
+ stub_feature_flags(cache_pages_domain_api: false)
+ end
+
+ it 'returns the virual domain when there are pages deployed for the project' do
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths.length).to eq(1)
+ expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id)
+ end
+ end
+ end
+ end
+
+ context 'when pages unique domain is disabled' do
+ before_all do
+ project.project_setting.update!(pages_unique_domain_enabled: false)
+ end
+
+ context 'when there are no pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+
+ context 'when there are pages deployed for the project' do
+ before_all do
+ project.mark_pages_as_deployed
+ end
+
+ it 'returns nil' do
+ expect(virtual_domain).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 6f445de8aee..d529319e6e9 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1047,41 +1047,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic
end
end
- describe '#action_monthly_active_users', :clean_gitlab_redis_shared_state do
- let(:time_period) { { created_at: 2.days.ago..time } }
- let(:time) { Time.zone.now }
- let(:user1) { build(:user, id: 1) }
- let(:user2) { build(:user, id: 2) }
- let(:user3) { build(:user, id: 3) }
- let(:user4) { build(:user, id: 4) }
- let(:project) { build(:project) }
-
- before do
- counter = Gitlab::UsageDataCounters::EditorUniqueCounter
-
- counter.track_web_ide_edit_action(author: user1, project: project)
- counter.track_web_ide_edit_action(author: user1, project: project)
- counter.track_sfe_edit_action(author: user1, project: project)
- counter.track_snippet_editor_edit_action(author: user1, project: project)
- counter.track_snippet_editor_edit_action(author: user1, time: time - 3.days, project: project)
-
- counter.track_web_ide_edit_action(author: user2, project: project)
- counter.track_sfe_edit_action(author: user2, project: project)
-
- counter.track_web_ide_edit_action(author: user3, time: time - 3.days, project: project)
- counter.track_snippet_editor_edit_action(author: user3, project: project)
- end
-
- it 'returns the distinct count of user actions within the specified time period' do
- expect(described_class.action_monthly_active_users(time_period)).to eq(
- {
- action_monthly_active_users_sfe_edit: 2,
- action_monthly_active_users_snippet_editor_edit: 2
- }
- )
- end
- end
-
describe '.service_desk_counts' do
subject { described_class.send(:service_desk_counts) }
diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb
index 2c75d4d5c41..75c5cac899b 100644
--- a/spec/models/concerns/each_batch_spec.rb
+++ b/spec/models/concerns/each_batch_spec.rb
@@ -171,4 +171,36 @@ RSpec.describe EachBatch do
end
end
end
+
+ describe '.each_batch_count' do
+ let_it_be(:users) { create_list(:user, 5, updated_at: 1.day.ago) }
+
+ it 'counts the records' do
+ count, last_value = User.each_batch_count
+
+ expect(count).to eq(5)
+ expect(last_value).to eq(nil)
+ end
+
+ context 'when using a different column' do
+ it 'returns correct count' do
+ count, _ = User.each_batch_count(column: :email, of: 2)
+
+ expect(count).to eq(5)
+ end
+ end
+
+ context 'when stopping and resuming the counting' do
+ it 'returns the correct count' do
+ count, last_value = User.each_batch_count(of: 1) do |current_count, _current_value|
+ current_count == 3 # stop when count reaches 3
+ end
+
+ expect(count).to eq(3)
+
+ final_count, _ = User.each_batch_count(of: 1, last_value: last_value, last_count: count)
+ expect(final_count).to eq(5)
+ end
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 60338d42bfa..a6d94222b65 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -1137,43 +1137,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe '.find_by_pages_host' do
- it 'finds namespace by GitLab Pages host and is case-insensitive' do
- namespace = create(:namespace, name: 'topNAMEspace', path: 'topNAMEspace')
- create(:namespace, name: 'annother_namespace')
- host = "TopNamespace.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to eq(namespace)
- end
-
- context 'when there is non-top-level group with searched name' do
- before do
- create(:group, :nested, path: 'pages')
- end
-
- it 'ignores this group' do
- host = "pages.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to be_nil
- end
-
- it 'finds right top level group' do
- group = create(:group, path: 'pages')
-
- host = "pages.#{Settings.pages.host.upcase}"
-
- expect(described_class.find_by_pages_host(host)).to eq(group)
- end
- end
-
- it "returns no result if the provided host is not subdomain of the Pages host" do
- create(:namespace, name: 'namespace.io')
- host = "namespace.io"
-
- expect(described_class.find_by_pages_host(host)).to eq(nil)
- end
- end
-
describe '.top_most' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:group) { create(:group) }
@@ -2290,34 +2253,6 @@ RSpec.describe Namespace, feature_category: :subgroups do
end
end
- describe '#pages_virtual_domain' do
- let(:project) { create(:project, namespace: namespace) }
- let(:virtual_domain) { namespace.pages_virtual_domain }
-
- before do
- project.mark_pages_as_deployed
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- it 'returns the virual domain' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{namespace.root_ancestor.id}_/)
- end
-
- context 'when :cache_pages_domain_api is disabled' do
- before do
- stub_feature_flags(cache_pages_domain_api: false)
- end
-
- it 'returns the virual domain' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to be_nil
- end
- end
- end
-
describe '#any_project_with_pages_deployed?' do
it 'returns true if any project nested under the group has pages deployed' do
parent_1 = create(:group) # Three projects, one with pages
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index ef79ba28d5d..38ff1bb090e 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -61,15 +61,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses deployment from object storage' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: deployment.file.url(expire_at: 1.day.from_now),
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: deployment.file.url(expire_at: 1.day.from_now),
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -87,15 +85,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses file protocol' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: 'file://' + deployment.file.path,
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: "file://#{deployment.file.path}",
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -108,15 +104,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
it 'uses deployment from object storage' do
freeze_time do
- expect(source).to(
- eq({
- type: 'zip',
- path: deployment.file.url(expire_at: 1.day.from_now),
- global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
- sha256: deployment.file_sha256,
- file_size: deployment.size,
- file_count: deployment.file_count
- })
+ expect(source).to eq(
+ type: 'zip',
+ path: deployment.file.url(expire_at: 1.day.from_now),
+ global_id: "gid://gitlab/PagesDeployment/#{deployment.id}",
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
)
end
end
@@ -143,4 +137,25 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do
expect(lookup_path.prefix).to eq('/myproject/')
end
end
+
+ describe '#unique_domain' do
+ let(:project) { build(:project) }
+
+ context 'when unique domain is disabled' do
+ it 'returns nil' do
+ project.project_setting.pages_unique_domain_enabled = false
+
+ expect(lookup_path.unique_domain).to be_nil
+ end
+ end
+
+ context 'when unique domain is enabled' do
+ it 'returns the project unique domain' do
+ project.project_setting.pages_unique_domain_enabled = true
+ project.project_setting.pages_unique_domain = 'unique-domain'
+
+ expect(lookup_path.unique_domain).to eq('unique-domain')
+ end
+ end
+ end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index f054fde78e7..ee198f73785 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -546,44 +546,6 @@ RSpec.describe PagesDomain do
end
end
- describe '#pages_virtual_domain' do
- let(:project) { create(:project) }
- let(:pages_domain) { create(:pages_domain, project: project) }
-
- context 'when there are no pages deployed for the project' do
- it 'returns nil' do
- expect(pages_domain.pages_virtual_domain).to be_nil
- end
- end
-
- context 'when there are pages deployed for the project' do
- let(:virtual_domain) { pages_domain.pages_virtual_domain }
-
- before do
- project.mark_pages_as_deployed
- project.update_pages_deployment!(create(:pages_deployment, project: project))
- end
-
- it 'returns the virual domain when there are pages deployed for the project' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to match(/pages_domain_for_domain_#{pages_domain.id}_/)
- end
-
- context 'when :cache_pages_domain_api is disabled' do
- before do
- stub_feature_flags(cache_pages_domain_api: false)
- end
-
- it 'returns the virual domain when there are pages deployed for the project' do
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- expect(virtual_domain.cache_key).to be_nil
- end
- end
- end
- end
-
describe '#validate_custom_domain_count_per_project' do
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index f68ea0cd84c..c9341934ec9 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -141,7 +141,11 @@ RSpec.describe API::Files, feature_category: :source_code_management do
it 'caches sha256 of the content', :use_clean_rails_redis_caching do
head api(route(file_path), current_user, **options), params: params
- expect(Gitlab::Cache::Client).to receive(:build_with_metadata).and_call_original
+ expect(Gitlab::Cache::Client).to receive(:build_with_metadata).with(
+ cache_identifier: 'API::Files#content_sha',
+ feature_category: :source_code_management,
+ backing_resource: :gitaly
+ ).and_call_original
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
.to eq(content_sha256)
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 56f1089843b..67f5b7f8ccb 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -212,7 +212,98 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'sha256' => deployment.file_sha256,
'file_size' => deployment.size,
'file_count' => deployment.file_count
- }
+ },
+ 'unique_domain' => nil
+ }
+ ]
+ )
+ end
+ end
+ end
+
+ context 'unique domain' do
+ let(:project) { create(:project) }
+
+ before do
+ project.project_setting.update!(
+ pages_unique_domain: 'unique-domain',
+ pages_unique_domain_enabled: true)
+ end
+
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ query_host('unique-domain.example.com')
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when there are pages deployed for the related project' do
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
+
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ deploy_pages(project)
+
+ query_host('unique-domain.example.com')
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ context 'when the unique domain is disabled' do
+ before do
+ project.project_setting.update!(pages_unique_domain_enabled: false)
+ end
+
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ deploy_pages(project)
+
+ query_host('unique-domain.example.com')
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ it 'domain lookup is case insensitive' do
+ deploy_pages(project)
+
+ query_host('Unique-Domain.example.com')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'responds with the correct domain configuration' do
+ deploy_pages(project)
+
+ query_host('unique-domain.example.com')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ deployment = project.pages_metadatum.pages_deployment
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => '/',
+ 'source' => {
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
+ },
+ 'unique_domain' => 'unique-domain'
}
]
)
@@ -253,7 +344,8 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'sha256' => deployment.file_sha256,
'file_size' => deployment.size,
'file_count' => deployment.file_count
- }
+ },
+ 'unique_domain' => nil
}
]
)
@@ -299,7 +391,8 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'sha256' => deployment.file_sha256,
'file_size' => deployment.size,
'file_count' => deployment.file_count
- }
+ },
+ 'unique_domain' => nil
}
]
)
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index d82fa284a42..54ee5e489f7 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
.to change { Gitlab::GitalyClient.get_request_count }.by_at_most(4)
end
- context 'caching', :use_clean_rails_memory_store_caching do
+ context 'caching' do
let(:reference) { create(:issue, project: project) }
let(:author) { create(:user) }
let!(:first_note) { create(:diff_note_on_merge_request, author: author, noteable: merge_request, project: project, note: "reference: #{reference.to_reference}") }
@@ -81,193 +81,180 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
shared_examples 'cache hit' do
it 'gets cached on subsequent requests' do
- expect_next_instance_of(DiscussionSerializer) do |serializer|
- expect(serializer).not_to receive(:represent)
- end
+ expect(DiscussionSerializer).not_to receive(:new)
send_request
end
end
- context 'when mr_discussions_http_cache and disabled_mr_discussions_redis_cache are enabled' do
- before do
- send_request
- end
-
- it_behaves_like 'cache hit'
+ before do
+ send_request
+ end
- context 'when a note in a discussion got updated' do
- before do
- first_note.update!(updated_at: 1.minute.from_now)
- end
+ it_behaves_like 'cache hit'
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ context 'when a note in a discussion got updated' do
+ before do
+ first_note.update!(updated_at: 1.minute.from_now)
end
- context 'when a note in a discussion got its reference state updated' do
- before do
- reference.close!
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ context 'when a note in a discussion got its reference state updated' do
+ before do
+ reference.close!
end
- context 'when a note in a discussion got resolved' do
- before do
- travel_to(1.minute.from_now) do
- first_note.resolve!(user)
- end
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
+ context 'when a note in a discussion got resolved' do
+ before do
+ travel_to(1.minute.from_now) do
+ first_note.resolve!(user)
end
end
- context 'when a note is added to a discussion' do
- let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) }
-
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note, third_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when a note is removed from a discussion' do
- before do
- second_note.destroy!
- end
+ context 'when a note is added to a discussion' do
+ let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) }
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note, third_note] }
end
+ end
- context 'when an emoji is awarded to a note in discussion' do
- before do
- travel_to(1.minute.from_now) do
- create(:award_emoji, awardable: first_note)
- end
- end
+ context 'when a note is removed from a discussion' do
+ before do
+ second_note.destroy!
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note] }
end
+ end
- context 'when an award emoji is removed from a note in discussion' do
- before do
- travel_to(1.minute.from_now) do
- award_emoji.destroy!
- end
+ context 'when an emoji is awarded to a note in discussion' do
+ before do
+ travel_to(1.minute.from_now) do
+ create(:award_emoji, awardable: first_note)
end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when the diff note position changes' do
- before do
- # This replicates a position change wherein timestamps aren't updated
- # which is why `Gitlab::Timeless.timeless` is utilized. This is the
- # same approach being used in Discussions::UpdateDiffPositionService
- # which is responsible for updating the positions of diff discussions
- # when MR updates.
- first_note.position = Gitlab::Diff::Position.new(
- old_path: first_note.position.old_path,
- new_path: first_note.position.new_path,
- old_line: first_note.position.old_line,
- new_line: first_note.position.new_line + 1,
- diff_refs: first_note.position.diff_refs
- )
-
- Gitlab::Timeless.timeless(first_note, &:save)
+ context 'when an award emoji is removed from a note in discussion' do
+ before do
+ travel_to(1.minute.from_now) do
+ award_emoji.destroy!
end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when the HEAD diff note position changes' do
- before do
- # This replicates a DiffNotePosition change. This is the same approach
- # being used in Discussions::CaptureDiffNotePositionService which is
- # responsible for updating/creating DiffNotePosition of a diff discussions
- # in relation to HEAD diff.
- new_position = Gitlab::Diff::Position.new(
- old_path: first_note.position.old_path,
- new_path: first_note.position.new_path,
- old_line: first_note.position.old_line,
- new_line: first_note.position.new_line + 1,
- diff_refs: first_note.position.diff_refs
- )
-
- DiffNotePosition.create_or_update_for(
- first_note,
- diff_type: :head,
- position: new_position,
- line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
- )
- end
+ context 'when the diff note position changes' do
+ before do
+ # This replicates a position change wherein timestamps aren't updated
+ # which is why `Gitlab::Timeless.timeless` is utilized. This is the
+ # same approach being used in Discussions::UpdateDiffPositionService
+ # which is responsible for updating the positions of diff discussions
+ # when MR updates.
+ first_note.position = Gitlab::Diff::Position.new(
+ old_path: first_note.position.old_path,
+ new_path: first_note.position.new_path,
+ old_line: first_note.position.old_line,
+ new_line: first_note.position.new_line + 1,
+ diff_refs: first_note.position.diff_refs
+ )
+
+ Gitlab::Timeless.timeless(first_note, &:save)
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author detail changes' do
- before do
- author.update!(name: "#{author.name} (Updated)")
- end
+ context 'when the HEAD diff note position changes' do
+ before do
+ # This replicates a DiffNotePosition change. This is the same approach
+ # being used in Discussions::CaptureDiffNotePositionService which is
+ # responsible for updating/creating DiffNotePosition of a diff discussions
+ # in relation to HEAD diff.
+ new_position = Gitlab::Diff::Position.new(
+ old_path: first_note.position.old_path,
+ new_path: first_note.position.new_path,
+ old_line: first_note.position.old_line,
+ new_line: first_note.position.new_line + 1,
+ diff_refs: first_note.position.diff_refs
+ )
+
+ DiffNotePosition.create_or_update_for(
+ first_note,
+ diff_type: :head,
+ position: new_position,
+ line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
+ )
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author status changes' do
- before do
- Users::SetStatusService.new(author, message: "updated status").execute
- end
+ context 'when author detail changes' do
+ before do
+ author.update!(name: "#{author.name} (Updated)")
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author role changes' do
- before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership)
- end
+ context 'when author status changes' do
+ before do
+ Users::SetStatusService.new(author, message: "updated status").execute
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when current_user role changes' do
- before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
- end
+ context 'when author role changes' do
+ before do
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership)
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
end
- context 'when disabled_mr_discussions_redis_cache is disabled' do
+ context 'when current_user role changes' do
before do
- stub_feature_flags(disabled_mr_discussions_redis_cache: false)
- send_request
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
end
- it_behaves_like 'cache hit'
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
index 122774a9028..565e79e14aa 100644
--- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -61,6 +61,20 @@ disabled_until: disabled_until)
# Nothing is missing
expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'causes all hooks to be considered executable' do
+ expect(find_hooks.executable.count).to eq(16)
+ end
+
+ it 'causes no hooks to be considered disabled' do
+ expect(find_hooks.disabled).to be_empty
+ end
+ end
end
describe '#executable?', :freeze_time do
@@ -108,6 +122,16 @@ disabled_until: disabled_until)
it 'has the correct state' do
expect(web_hook.executable?).to eq(executable)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is always executable' do
+ expect(web_hook).to be_executable
+ end
+ end
end
end
@@ -172,6 +196,16 @@ disabled_until: disabled_until)
def run_expectation
expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not increment backoff count' do
+ expect { hook.failed! }.not_to change { hook.backoff_count }
+ end
+ end
end
end
end
@@ -181,6 +215,16 @@ disabled_until: disabled_until)
def run_expectation
expect { hook.failed! }.to change { hook.recent_failures }.by(1)
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not increment recent failure count' do
+ expect { hook.failed! }.not_to change { hook.recent_failures }
+ end
+ end
end
end
@@ -189,6 +233,16 @@ disabled_until: disabled_until)
expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
end
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'does not disable the hook' do
+ expect { hook.disable! }.not_to change { hook.executable? }
+ end
+ end
+
it 'does nothing if the hook is already disabled' do
allow(hook).to receive(:permanently_disabled?).and_return(true)
@@ -228,6 +282,16 @@ disabled_until: disabled_until)
it 'is true' do
expect(hook).to be_temporarily_disabled
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is false' do
+ expect(hook).not_to be_temporarily_disabled
+ end
+ end
end
end
@@ -244,6 +308,16 @@ disabled_until: disabled_until)
it 'is true' do
expect(hook).to be_permanently_disabled
end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it 'is false' do
+ expect(hook).not_to be_permanently_disabled
+ end
+ end
end
end
@@ -258,6 +332,14 @@ disabled_until: disabled_until)
end
it { is_expected.to eq :disabled }
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it { is_expected.to eq(:executable) }
+ end
end
context 'when hook has been backed off' do
@@ -267,6 +349,14 @@ disabled_until: disabled_until)
end
it { is_expected.to eq :temporarily_disabled }
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
+
+ it { is_expected.to eq(:executable) }
+ end
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 1261eaa4cfb..74574163fea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1157,14 +1157,26 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.11.tgz#9be796d93ae27b636da32d960899a4912bca27a1"
integrity sha512-N9vXqLP3eRL8BqSy8yn4Y98cZI2pZ8fyuHx6lKjiG2WABpT2l01TXdzq5Ma2ZUBzfB7tx5dXVhge8X9u0S70ZQ==
-"@eslint/eslintrc@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.0.tgz#943309d8697c52fc82c076e90c1c74fbbe69dbff"
- integrity sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==
+"@eslint-community/eslint-utils@^4.2.0":
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518"
+ integrity sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==
+ dependencies:
+ eslint-visitor-keys "^3.3.0"
+
+"@eslint-community/regexpp@^4.4.0":
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403"
+ integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==
+
+"@eslint/eslintrc@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d"
+ integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
- espree "^9.4.0"
+ espree "^9.5.0"
globals "^13.19.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
@@ -1172,10 +1184,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
-"@eslint/js@8.35.0":
- version "8.35.0"
- resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.35.0.tgz#b7569632b0b788a0ca0e438235154e45d42813a7"
- integrity sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==
+"@eslint/js@8.36.0":
+ version "8.36.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe"
+ integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==
"@gitlab/at.js@1.5.7":
version "1.5.7"
@@ -5669,13 +5681,15 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
-eslint@8.35.0:
- version "8.35.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.35.0.tgz#fffad7c7e326bae606f0e8f436a6158566d42323"
- integrity sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==
+eslint@8.36.0:
+ version "8.36.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf"
+ integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==
dependencies:
- "@eslint/eslintrc" "^2.0.0"
- "@eslint/js" "8.35.0"
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@eslint-community/regexpp" "^4.4.0"
+ "@eslint/eslintrc" "^2.0.1"
+ "@eslint/js" "8.36.0"
"@humanwhocodes/config-array" "^0.11.8"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
@@ -5686,9 +5700,8 @@ eslint@8.35.0:
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.1.1"
- eslint-utils "^3.0.0"
eslint-visitor-keys "^3.3.0"
- espree "^9.4.0"
+ espree "^9.5.0"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@@ -5710,15 +5723,14 @@ eslint@8.35.0:
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.1"
- regexpp "^3.2.0"
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
-espree@^9.3.1, espree@^9.4.0:
- version "9.4.0"
- resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a"
- integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==
+espree@^9.3.1, espree@^9.5.0:
+ version "9.5.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113"
+ integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==
dependencies:
acorn "^8.8.0"
acorn-jsx "^5.3.2"
@@ -10602,11 +10614,6 @@ regexp.prototype.flags@^1.4.3:
define-properties "^1.1.3"
functions-have-names "^1.2.2"
-regexpp@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
- integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
-
regexpu-core@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3"