summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-17 15:10:15 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-17 15:10:15 +0000
commit68c476dbd8a2c670aeeebffce8b63b554a3ac7f0 (patch)
treec46b90a5c131d8e8d7fb530f1b8f9390b8eb2613
parenta62238de7302e54edafa3407a2dc8ba1a5f96e4d (diff)
downloadgitlab-ce-68c476dbd8a2c670aeeebffce8b63b554a3ac7f0.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/build-images.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml2
-rw-r--r--.rubocop_manual_todo.yml1
-rw-r--r--.rubocop_todo.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue2
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/finders/packages/group_packages_finder.rb1
-rw-r--r--app/finders/packages/nuget/package_finder.rb1
-rw-r--r--app/finders/packages/package_finder.rb1
-rw-r--r--app/finders/packages/packages_finder.rb1
-rw-r--r--app/finders/packages/pypi/package_finder.rb2
-rw-r--r--app/finders/packages/pypi/packages_finder.rb2
-rw-r--r--app/models/integrations/chat_message/alert_message.rb76
-rw-r--r--app/models/integrations/chat_message/base_message.rb88
-rw-r--r--app/models/integrations/chat_message/deployment_message.rb87
-rw-r--r--app/models/integrations/chat_message/issue_message.rb74
-rw-r--r--app/models/integrations/chat_message/merge_message.rb83
-rw-r--r--app/models/integrations/chat_message/note_message.rb86
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb267
-rw-r--r--app/models/integrations/chat_message/push_message.rb120
-rw-r--r--app/models/integrations/chat_message/wiki_page_message.rb63
-rw-r--r--app/models/packages/package.rb5
-rw-r--r--app/models/pages/lookup_path.rb9
-rw-r--r--app/models/pages/virtual_domain.rb4
-rw-r--r--app/models/project_services/chat_message/alert_message.rb74
-rw-r--r--app/models/project_services/chat_message/base_message.rb86
-rw-r--r--app/models/project_services/chat_message/deployment_message.rb85
-rw-r--r--app/models/project_services/chat_message/issue_message.rb72
-rw-r--r--app/models/project_services/chat_message/merge_message.rb81
-rw-r--r--app/models/project_services/chat_message/note_message.rb84
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb265
-rw-r--r--app/models/project_services/chat_message/push_message.rb118
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb61
-rw-r--r--app/models/project_services/chat_notification_service.rb14
-rw-r--r--app/models/project_services/slack_service.rb2
-rw-r--r--app/services/issuable/destroy_service.rb20
-rw-r--r--app/services/spam/spam_verdict_service.rb16
-rw-r--r--app/views/notify/change_in_merge_request_draft_status_email.html.haml4
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml2
-rw-r--r--changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml5
-rw-r--r--changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml5
-rw-r--r--changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml5
-rw-r--r--changelogs/unreleased/325689-remove-delete-async-ffs.yml5
-rw-r--r--changelogs/unreleased/329521-store-segment-target-groups.yml5
-rw-r--r--changelogs/unreleased/330845-observe-limit-to-hours.yml5
-rw-r--r--changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml5
-rw-r--r--changelogs/unreleased/cablett-change-draft-status-email.yml5
-rw-r--r--changelogs/unreleased/grape-action-caching-default.yml5
-rw-r--r--changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml5
-rw-r--r--config/feature_flags/development/api_caching_rate_limit_branches.yml2
-rw-r--r--config/feature_flags/development/destroy_issuable_label_links_async.yml8
-rw-r--r--config/feature_flags/development/destroy_issuable_todos_async.yml8
-rw-r--r--config/feature_flags/development/pages_serve_from_legacy_storage.yml8
-rw-r--r--config/feature_flags/development/pages_update_legacy_storage.yml8
-rw-r--r--config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml22
-rw-r--r--config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml22
-rw-r--r--config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml21
-rw-r--r--config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml21
-rw-r--r--config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml22
-rw-r--r--config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml21
-rw-r--r--config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml21
-rw-r--r--config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml22
-rw-r--r--config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml21
-rw-r--r--config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml21
-rw-r--r--config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml22
-rw-r--r--config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml21
-rw-r--r--config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml21
-rw-r--r--db/migrate/20210430122951_add_snapshot_namespace_id.rb7
-rw-r--r--db/migrate/20210430124212_add_display_namespace_id_to_segments.rb7
-rw-r--r--db/migrate/20210430124630_add_devops_adoption_indexes.rb32
-rw-r--r--db/post_migrate/20210430130259_remove_obsolete_segments_field.rb18
-rw-r--r--db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb16
-rw-r--r--db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb14
-rw-r--r--db/post_migrate/20210511051718_create_index_on_notes_note.rb26
-rw-r--r--db/schema_migrations/202104301229511
-rw-r--r--db/schema_migrations/202104301242121
-rw-r--r--db/schema_migrations/202104301246301
-rw-r--r--db/schema_migrations/202104301302591
-rw-r--r--db/schema_migrations/202104301342021
-rw-r--r--db/schema_migrations/202104301359541
-rw-r--r--db/schema_migrations/202105110517181
-rw-r--r--db/structure.sql20
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md1
-rw-r--r--doc/administration/pages/index.md52
-rw-r--r--doc/api/epic_links.md2
-rw-r--r--doc/development/testing_guide/best_practices.md21
-rw-r--r--doc/development/usage_ping/dictionary.md288
-rw-r--r--doc/user/group/epics/img/epic_view_v13.0.pngbin51112 -> 0 bytes
-rw-r--r--doc/user/group/epics/img/new_epic_from_groups_v13.7.pngbin10505 -> 0 bytes
-rw-r--r--doc/user/group/epics/index.md157
-rw-r--r--doc/user/group/epics/manage_epics.md73
-rw-r--r--doc/user/project/issues/managing_issues.md7
-rw-r--r--lib/banzai/filter/references/snippet_reference_filter.rb8
-rw-r--r--lib/gitlab/database/reindexing/concurrent_reindex.rb18
-rw-r--r--lib/gitlab/database/with_lock_retries.rb4
-rw-r--r--lib/gitlab/database/with_lock_retries_outside_transaction.rb41
-rw-r--r--lib/gitlab/pages/settings.rb4
-rw-r--r--lib/gitlab/pages/stores/local_store.rb15
-rw-r--r--lib/gitlab/usage_data.rb25
-rw-r--r--locale/gitlab.pot5
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/resource/group.rb43
-rw-r--r--qa/qa/resource/group_base.rb74
-rw-r--r--qa/qa/resource/sandbox.rb38
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb39
-rwxr-xr-xscripts/trigger-build2
-rw-r--r--spec/features/groups/issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/service_desk_spec.rb2
-rw-r--r--spec/finders/packages/group_packages_finder_spec.rb2
-rw-r--r--spec/finders/packages/package_finder_spec.rb2
-rw-r--r--spec/finders/packages/packages_finder_spec.rb2
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js30
-rw-r--r--spec/lib/banzai/filter/references/reference_cache_spec.rb1
-rw-r--r--spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb27
-rw-r--r--spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb36
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb244
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb18
-rw-r--r--spec/lib/gitlab/pages/settings_spec.rb8
-rw-r--r--spec/lib/gitlab/pages/stores/local_store_spec.rb25
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb80
-rw-r--r--spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb47
-rw-r--r--spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb25
-rw-r--r--spec/models/integrations/chat_message/alert_message_spec.rb (renamed from spec/models/project_services/chat_message/alert_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/base_message_spec.rb (renamed from spec/models/project_services/chat_message/base_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/deployment_message_spec.rb (renamed from spec/models/project_services/chat_message/deployment_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/issue_message_spec.rb (renamed from spec/models/project_services/chat_message/issue_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/merge_message_spec.rb (renamed from spec/models/project_services/chat_message/merge_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/note_message_spec.rb (renamed from spec/models/project_services/chat_message/note_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/pipeline_message_spec.rb (renamed from spec/models/project_services/chat_message/pipeline_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/push_message_spec.rb (renamed from spec/models/project_services/chat_message/push_message_spec.rb)2
-rw-r--r--spec/models/integrations/chat_message/wiki_page_message_spec.rb (renamed from spec/models/project_services/chat_message/wiki_page_message_spec.rb)2
-rw-r--r--spec/models/packages/package_spec.rb16
-rw-r--r--spec/models/pages/lookup_path_spec.rb7
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb2
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb31
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb40
-rw-r--r--spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb3
156 files changed, 2904 insertions, 1427 deletions
diff --git a/.gitlab/ci/build-images.gitlab-ci.yml b/.gitlab/ci/build-images.gitlab-ci.yml
index 4e352472047..ed1f71e27bb 100644
--- a/.gitlab/ci/build-images.gitlab-ci.yml
+++ b/.gitlab/ci/build-images.gitlab-ci.yml
@@ -9,8 +9,18 @@ build-qa-image:
- .build-images:rules:build-qa-image
stage: build-images
needs: []
+ variables:
+ QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
script:
- - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
+ # With .git/hooks/post-checkout in place, Git tries to pull LFS objects, but the image doesn't have Git LFS, and we actually don't care about it for this specific so we just remove the file.
+ # Without removing the file, the error is as follows: "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout."
+ - rm .git/hooks/post-checkout
+ # Use $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA so that GitLab image built in omnibus-gitlab-mirror and QA image are in sync.
+ # This falls back to $CI_COMMIT_SHA (the default checked out commit) for the non-merged result pipelines.
+ # See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/index.html#with-pipeline-for-merged-results.
+ - if [ -n "$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA" ]; then
+ git checkout -f ${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA};
+ fi
- /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --cache=true
retry: 2
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 891457afe6e..0cadce5cc20 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -128,7 +128,7 @@
.use-kaniko:
image:
- name: gcr.io/kaniko-project/executor:debug-v1.3.0
+ name: registry.gitlab.com/gitlab-org/gitlab-build-images:kaniko
entrypoint: [""]
before_script:
- source scripts/utils.sh
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 778a39c9a6e..2ec3df1aeda 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -2941,7 +2941,6 @@ Style/RegexpLiteralMixedPreserve:
- 'app/models/concerns/ci/maskable.rb'
- 'app/models/operations/feature_flag.rb'
- 'app/models/packages/go/module.rb'
- - 'app/models/project_services/chat_message/base_message.rb'
- 'app/services/packages/conan/search_service.rb'
- 'app/services/projects/update_remote_mirror_service.rb'
- 'config/initializers/rspec_profiling.rb'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 0f2f3bc44a4..26b12f26943 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -598,7 +598,6 @@ Rails/RenderInline:
# SupportedStyles: conservative, aggressive
Rails/ShortI18n:
Exclude:
- - 'app/models/project_services/chat_message/pipeline_message.rb'
- 'app/uploaders/content_type_whitelist.rb'
# Offense count: 1144
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index dcc93f19e1c..ffe37cdc2cf 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-2be01682b4a0ba12cad12b30bad4dc8876503f22
+c93530dd0922e7554d6d1e486e830f72980fe083
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 04d80dc0069..ad557f64ce4 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -333,7 +333,7 @@ export default {
ghostClass: 'gl-visibility-hidden',
},
i18n: {
- dropzoneDescriptionText: __('Drop or %{linkStart}upload%{linkEnd} designs to attach'),
+ dropzoneDescriptionText: __('Drag your designs here or %{linkStart}click to upload%{linkEnd}.'),
},
};
</script>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 6764c5df06a..67242b3b5b7 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -14,6 +14,13 @@ export default {
GlTable,
},
inject: ['issuableId', 'issuableType'],
+ props: {
+ limitToHours: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
data() {
return { report: [], isLoading: true };
},
@@ -60,7 +67,10 @@ export default {
},
formatTimeSpent(seconds) {
const negative = seconds < 0;
- return (negative ? '- ' : '') + stringifyTime(parseSeconds(seconds));
+ return (
+ (negative ? '- ' : '') +
+ stringifyTime(parseSeconds(seconds, { limitToHours: this.limitToHours }))
+ );
},
},
fields: [
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index ac3d278d840..64f2ddc1d16 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -180,7 +180,7 @@ export default {
:title="__('Time tracking report')"
:hide-footer="true"
>
- <time-tracking-report />
+ <time-tracking-report :limit-to-hours="limitToHours" />
</gl-modal>
<transition name="help-state-toggle">
<time-tracking-help-state v-if="showHelpState" />
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index c2e879b45bc..86788a84260 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -178,7 +178,7 @@ export default {
class="labels-fetch-loading gl-align-items-center w-100 h-100"
size="md"
/>
- <ul v-else class="list-unstyled mb-0">
+ <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word">
<label-item
v-for="(label, index) in visibleLabels"
:key="label.id"
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 7c9572568f1..227dd0591d4 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -14,7 +14,7 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html
- feature_category :audit_events, [:activity]
+ feature_category :users, [:activity]
feature_category :issue_tracking, [:issues, :issues_calendar]
feature_category :code_review, [:merge_requests]
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 3679313b152..a755d242d4a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -49,10 +49,9 @@ class GroupsController < Groups::ApplicationController
feature_category :subgroups, [
:index, :new, :create, :show, :edit, :update,
- :destroy, :details, :transfer
+ :destroy, :details, :transfer, :activity
]
- feature_category :audit_events, [:activity]
feature_category :issue_tracking, [:issues, :issues_calendar, :preview_markdown]
feature_category :code_review, [:merge_requests, :unfoldered_environment_names]
feature_category :projects, [:projects]
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ad407e76b7a..e66893ac269 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -43,13 +43,12 @@ class ProjectsController < Projects::ApplicationController
feature_category :projects, [
:index, :show, :new, :create, :edit, :update, :transfer,
- :destroy, :resolve, :archive, :unarchive, :toggle_star
+ :destroy, :resolve, :archive, :unarchive, :toggle_star, :activity
]
feature_category :source_code_management, [:remove_fork, :housekeeping, :refs]
feature_category :issue_tracking, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
- feature_category :audit_events, [:activity]
feature_category :code_review, [:unfoldered_environment_names]
def index
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index cce2e054459..e753fa4d455 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -26,7 +26,6 @@ module Packages
.including_project_route
.including_tags
.for_projects(group_projects_visible_to_current_user.select(:id))
- .processed
.sort_by_attribute("#{params[:order_by]}_#{params[:sort]}")
packages = filter_with_version(packages)
diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb
index f5eb60f2931..9ae52745bb2 100644
--- a/app/finders/packages/nuget/package_finder.rb
+++ b/app/finders/packages/nuget/package_finder.rb
@@ -14,7 +14,6 @@ module Packages
def packages
result = base.nuget
.has_version
- .processed
.with_name_like(@params[:package_name])
result = result.with_version(@params[:package_version]) if @params[:package_version].present?
result
diff --git a/app/finders/packages/package_finder.rb b/app/finders/packages/package_finder.rb
index 368d2028cb5..ee96896e350 100644
--- a/app/finders/packages/package_finder.rb
+++ b/app/finders/packages/package_finder.rb
@@ -13,7 +13,6 @@ module Packages
.including_project_route
.including_tags
.displayable
- .processed
.find(@package_id)
end
end
diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb
index 840cbbf7b9d..552468ecfd1 100644
--- a/app/finders/packages/packages_finder.rb
+++ b/app/finders/packages/packages_finder.rb
@@ -17,7 +17,6 @@ module Packages
.including_build_info
.including_project_route
.including_tags
- .processed
packages = filter_with_version(packages)
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
diff --git a/app/finders/packages/pypi/package_finder.rb b/app/finders/packages/pypi/package_finder.rb
index 3bb484b34f2..574e9770363 100644
--- a/app/finders/packages/pypi/package_finder.rb
+++ b/app/finders/packages/pypi/package_finder.rb
@@ -10,7 +10,7 @@ module Packages
private
def packages
- base.pypi.has_version.processed
+ base.pypi.has_version
end
end
end
diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb
index 223caecb4dc..642ca2cf2e6 100644
--- a/app/finders/packages/pypi/packages_finder.rb
+++ b/app/finders/packages/pypi/packages_finder.rb
@@ -13,7 +13,7 @@ module Packages
private
def packages
- base.pypi.has_version.processed
+ base.pypi.has_version
end
end
end
diff --git a/app/models/integrations/chat_message/alert_message.rb b/app/models/integrations/chat_message/alert_message.rb
new file mode 100644
index 00000000000..ef0579124fe
--- /dev/null
+++ b/app/models/integrations/chat_message/alert_message.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class AlertMessage < BaseMessage
+ attr_reader :title
+ attr_reader :alert_url
+ attr_reader :severity
+ attr_reader :events
+ attr_reader :status
+ attr_reader :started_at
+
+ def initialize(params)
+ @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
+ @project_url = params.dig(:project, :web_url) || params[:project_url]
+ @title = params.dig(:object_attributes, :title)
+ @alert_url = params.dig(:object_attributes, :url)
+ @severity = params.dig(:object_attributes, :severity)
+ @events = params.dig(:object_attributes, :events)
+ @status = params.dig(:object_attributes, :status)
+ @started_at = params.dig(:object_attributes, :started_at)
+ end
+
+ def attachments
+ [{
+ title: title,
+ title_link: alert_url,
+ color: attachment_color,
+ fields: attachment_fields
+ }]
+ end
+
+ def message
+ "Alert firing in #{project_name}"
+ end
+
+ private
+
+ def attachment_color
+ "#C95823"
+ end
+
+ def attachment_fields
+ [
+ {
+ title: "Severity",
+ value: severity.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Events",
+ value: events,
+ short: true
+ },
+ {
+ title: "Status",
+ value: status.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Start time",
+ value: format_time(started_at),
+ short: true
+ }
+ ]
+ end
+
+ # This formats time into the following format
+ # April 23rd, 2020 1:06AM UTC
+ def format_time(time)
+ time = Time.zone.parse(time.to_s)
+ time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/base_message.rb b/app/models/integrations/chat_message/base_message.rb
new file mode 100644
index 00000000000..2f70384d3b9
--- /dev/null
+++ b/app/models/integrations/chat_message/base_message.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class BaseMessage
+ RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze
+
+ attr_reader :markdown
+ attr_reader :user_full_name
+ attr_reader :user_name
+ attr_reader :user_avatar
+ attr_reader :project_name
+ attr_reader :project_url
+
+ def initialize(params)
+ @markdown = params[:markdown] || false
+ @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
+ @project_url = params.dig(:project, :web_url) || params[:project_url]
+ @user_full_name = params.dig(:user, :name) || params[:user_full_name]
+ @user_name = params.dig(:user, :username) || params[:user_name]
+ @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
+ end
+
+ def user_combined_name
+ if user_full_name.present?
+ "#{user_full_name} (#{user_name})"
+ else
+ user_name
+ end
+ end
+
+ def summary
+ return message if markdown
+
+ format(message)
+ end
+
+ def pretext
+ summary
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ raise NotImplementedError
+ end
+
+ def activity
+ raise NotImplementedError
+ end
+
+ private
+
+ def message
+ raise NotImplementedError
+ end
+
+ def format(string)
+ Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
+ end
+
+ def format_relative_links(string)
+ string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
+ end
+
+ def attachment_color
+ '#345'
+ end
+
+ def link(text, url)
+ "[#{text}](#{url})"
+ end
+
+ def pretty_duration(seconds)
+ parse_string =
+ if duration < 1.hour
+ '%M:%S'
+ else
+ '%H:%M:%S'
+ end
+
+ Time.at(seconds).utc.strftime(parse_string)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb
new file mode 100644
index 00000000000..c4f3bf9610d
--- /dev/null
+++ b/app/models/integrations/chat_message/deployment_message.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class DeploymentMessage < BaseMessage
+ attr_reader :commit_title
+ attr_reader :commit_url
+ attr_reader :deployable_id
+ attr_reader :deployable_url
+ attr_reader :environment
+ attr_reader :short_sha
+ attr_reader :status
+ attr_reader :user_url
+
+ def initialize(data)
+ super
+
+ @commit_title = data[:commit_title]
+ @commit_url = data[:commit_url]
+ @deployable_id = data[:deployable_id]
+ @deployable_url = data[:deployable_url]
+ @environment = data[:environment]
+ @short_sha = data[:short_sha]
+ @status = data[:status]
+ @user_url = data[:user_url]
+ end
+
+ def attachments
+ [{
+ text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
+ color: color
+ }]
+ end
+
+ def activity
+ {}
+ end
+
+ private
+
+ def message
+ if running?
+ "Starting deploy to #{environment}"
+ else
+ "Deploy to #{environment} #{humanized_status}"
+ end
+ end
+
+ def color
+ case status
+ when 'success'
+ 'good'
+ when 'canceled'
+ 'warning'
+ when 'failed'
+ 'danger'
+ else
+ '#334455'
+ end
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def deployment_link
+ link("##{deployable_id}", deployable_url)
+ end
+
+ def user_link
+ link(user_combined_name, user_url)
+ end
+
+ def commit_link
+ link(short_sha, commit_url)
+ end
+
+ def humanized_status
+ status == 'success' ? 'succeeded' : status
+ end
+
+ def running?
+ status == 'running'
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb
new file mode 100644
index 00000000000..5fa6bd4090f
--- /dev/null
+++ b/app/models/integrations/chat_message/issue_message.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class IssueMessage < BaseMessage
+ attr_reader :title
+ attr_reader :issue_iid
+ attr_reader :issue_url
+ attr_reader :action
+ attr_reader :state
+ attr_reader :description
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @issue_iid = obj_attr[:iid]
+ @issue_url = obj_attr[:url]
+ @action = obj_attr[:action]
+ @state = obj_attr[:state]
+ @description = obj_attr[:description] || ''
+ end
+
+ def attachments
+ return [] unless opened_issue?
+ return description if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "Issue #{state} by #{user_combined_name}",
+ subtitle: "in #{project_link}",
+ text: issue_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
+ end
+
+ def opened_issue?
+ action == 'open'
+ end
+
+ def description_message
+ [{
+ title: issue_title,
+ title_link: issue_url,
+ text: format(description),
+ color: '#C95823'
+ }]
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def issue_link
+ link(issue_title, issue_url)
+ end
+
+ def issue_title
+ "#{Issue.reference_prefix}#{issue_iid} #{title}"
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/merge_message.rb b/app/models/integrations/chat_message/merge_message.rb
new file mode 100644
index 00000000000..d2f48699f50
--- /dev/null
+++ b/app/models/integrations/chat_message/merge_message.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class MergeMessage < BaseMessage
+ attr_reader :merge_request_iid
+ attr_reader :source_branch
+ attr_reader :target_branch
+ attr_reader :action
+ attr_reader :state
+ attr_reader :title
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @merge_request_iid = obj_attr[:iid]
+ @source_branch = obj_attr[:source_branch]
+ @target_branch = obj_attr[:target_branch]
+ @action = obj_attr[:action]
+ @state = obj_attr[:state]
+ @title = format_title(obj_attr[:title])
+ end
+
+ def attachments
+ []
+ end
+
+ def activity
+ {
+ title: "Merge request #{state_or_action_text} by #{user_combined_name}",
+ subtitle: "in #{project_link}",
+ text: merge_request_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def format_title(title)
+ '*' + title.lines.first.chomp + '*'
+ end
+
+ def message
+ merge_request_message
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def merge_request_message
+ "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
+ end
+
+ def merge_request_link
+ link(merge_request_title, merge_request_url)
+ end
+
+ def merge_request_title
+ "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
+ end
+
+ def merge_request_url
+ "#{project_url}/-/merge_requests/#{merge_request_iid}"
+ end
+
+ def state_or_action_text
+ case action
+ when 'approved', 'unapproved'
+ action
+ when 'approval'
+ 'added their approval to'
+ when 'unapproval'
+ 'removed their approval from'
+ else
+ state
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/note_message.rb b/app/models/integrations/chat_message/note_message.rb
new file mode 100644
index 00000000000..96675d2b27c
--- /dev/null
+++ b/app/models/integrations/chat_message/note_message.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class NoteMessage < BaseMessage
+ attr_reader :note
+ attr_reader :note_url
+ attr_reader :title
+ attr_reader :target
+
+ def initialize(params)
+ super
+
+ params = HashWithIndifferentAccess.new(params)
+ obj_attr = params[:object_attributes]
+ @note = obj_attr[:note]
+ @note_url = obj_attr[:url]
+ @target, @title = case obj_attr[:noteable_type]
+ when "Commit"
+ create_commit_note(params[:commit])
+ when "Issue"
+ create_issue_note(params[:issue])
+ when "MergeRequest"
+ create_merge_note(params[:merge_request])
+ when "Snippet"
+ create_snippet_note(params[:snippet])
+ end
+ end
+
+ def attachments
+ return note if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
+ subtitle: "in #{project_link}",
+ text: formatted_title,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
+ end
+
+ def format_title(title)
+ title.lines.first.chomp
+ end
+
+ def formatted_title
+ format_title(title)
+ end
+
+ def create_issue_note(issue)
+ ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
+ end
+
+ def create_commit_note(commit)
+ commit_sha = Commit.truncate_sha(commit[:id])
+
+ ["commit #{commit_sha}", commit[:message]]
+ end
+
+ def create_merge_note(merge_request)
+ ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
+ end
+
+ def create_snippet_note(snippet)
+ ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
+ end
+
+ def description_message
+ [{ text: format(note), color: attachment_color }]
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
new file mode 100644
index 00000000000..a0f6f582e4c
--- /dev/null
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -0,0 +1,267 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class PipelineMessage < BaseMessage
+ MAX_VISIBLE_JOBS = 10
+
+ attr_reader :user
+ attr_reader :ref_type
+ attr_reader :ref
+ attr_reader :status
+ attr_reader :detailed_status
+ attr_reader :duration
+ attr_reader :finished_at
+ attr_reader :pipeline_id
+ attr_reader :failed_stages
+ attr_reader :failed_jobs
+
+ attr_reader :project
+ attr_reader :commit
+ attr_reader :committer
+ attr_reader :pipeline
+
+ def initialize(data)
+ super
+
+ @user = data[:user]
+ @user_name = data.dig(:user, :username) || 'API'
+
+ pipeline_attributes = data[:object_attributes]
+ @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ @ref = pipeline_attributes[:ref]
+ @status = pipeline_attributes[:status]
+ @detailed_status = pipeline_attributes[:detailed_status]
+ @duration = pipeline_attributes[:duration].to_i
+ @finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
+ @pipeline_id = pipeline_attributes[:id]
+
+ # Get list of jobs that have actually failed (after exhausting all retries)
+ @failed_jobs = actually_failed_jobs(Array(data[:builds]))
+ @failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
+
+ @project = Project.find(data[:project][:id])
+ @commit = project.commit_by(oid: data[:commit][:id])
+ @committer = commit.committer
+ @pipeline = Ci::Pipeline.find(pipeline_id)
+ end
+
+ def pretext
+ ''
+ end
+
+ def attachments
+ return message if markdown
+
+ [{
+ fallback: format(message),
+ color: attachment_color,
+ author_name: user_combined_name,
+ author_icon: user_avatar,
+ author_link: author_url,
+ title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
+ {
+ pipeline_id: pipeline_id,
+ humanized_status: humanized_status,
+ duration: pretty_duration(duration)
+ },
+ title_link: pipeline_url,
+ fields: attachments_fields,
+ footer: project.name,
+ footer_icon: project.avatar_url(only_path: false),
+ ts: finished_at
+ }]
+ end
+
+ def activity
+ {
+ title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
+ {
+ pipeline_link: pipeline_link,
+ ref_type: ref_type,
+ ref_link: ref_link,
+ user_combined_name: user_combined_name,
+ humanized_status: humanized_status
+ },
+ subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
+ text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
+ image: user_avatar || ''
+ }
+ end
+
+ private
+
+ def actually_failed_jobs(builds)
+ succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
+
+ failed_jobs = builds.select do |build|
+ # Select jobs which doesn't have a successful retry
+ build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
+ end
+
+ failed_jobs.uniq { |job| job[:name] }.reverse
+ end
+
+ def failed_stages_field
+ {
+ title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
+ value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
+ short: true
+ }
+ end
+
+ def failed_jobs_field
+ {
+ title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
+ value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
+ short: true
+ }
+ end
+
+ def yaml_error_field
+ {
+ title: s_("ChatMessage|Invalid CI config YAML file"),
+ value: pipeline.yaml_errors,
+ short: false
+ }
+ end
+
+ def attachments_fields
+ fields = [
+ {
+ title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
+ value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
+ short: true
+ },
+ {
+ title: s_("ChatMessage|Commit"),
+ value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
+ short: true
+ }
+ ]
+
+ fields << failed_stages_field if failed_stages.any?
+ fields << failed_jobs_field if failed_jobs.any?
+ fields << yaml_error_field if pipeline.has_yaml_errors?
+
+ fields
+ end
+
+ def message
+ s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
+ {
+ project_link: project_link,
+ pipeline_link: pipeline_link,
+ ref_type: ref_type,
+ ref_link: ref_link,
+ user_combined_name: user_combined_name,
+ humanized_status: humanized_status,
+ duration: pretty_duration(duration)
+ }
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
+ when 'failed'
+ s_("ChatMessage|has failed")
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ case status
+ when 'success'
+ detailed_status == 'passed with warnings' ? 'warning' : 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/-/commits/#{ref}"
+ end
+ end
+
+ def ref_link
+ "[#{ref}](#{ref_url})"
+ end
+
+ def project_url
+ project.web_url
+ end
+
+ def project_link
+ "[#{project.name}](#{project_url})"
+ end
+
+ def pipeline_failed_jobs_url
+ "#{project_url}/-/pipelines/#{pipeline_id}/failures"
+ end
+
+ def pipeline_url
+ if failed_jobs.any?
+ pipeline_failed_jobs_url
+ else
+ "#{project_url}/-/pipelines/#{pipeline_id}"
+ end
+ end
+
+ def pipeline_link
+ "[##{pipeline_id}](#{pipeline_url})"
+ end
+
+ def job_url(job)
+ "#{project_url}/-/jobs/#{job[:id]}"
+ end
+
+ def job_link(job)
+ "[#{job[:name]}](#{job_url(job)})"
+ end
+
+ def failed_jobs_links
+ failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
+ truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
+
+ failed_links = failed.map { |job| job_link(job) }
+
+ unless truncated.blank?
+ failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
+ count: truncated.size,
+ pipeline_failed_jobs_url: pipeline_failed_jobs_url
+ }
+ end
+
+ failed_links.join(I18n.t(:'support.array.words_connector'))
+ end
+
+ def stage_link(stage)
+ # All stages link to the pipeline page
+ "[#{stage}](#{pipeline_url})"
+ end
+
+ def failed_stages_links
+ failed_stages.map { |s| stage_link(s) }.join(I18n.t(:'support.array.words_connector'))
+ end
+
+ def commit_url
+ Gitlab::UrlBuilder.build(commit)
+ end
+
+ def commit_link
+ "[#{commit.title}](#{commit_url})"
+ end
+
+ def author_url
+ return unless user && committer
+
+ Gitlab::UrlBuilder.build(committer)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
new file mode 100644
index 00000000000..0952986e923
--- /dev/null
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class PushMessage < BaseMessage
+ attr_reader :after
+ attr_reader :before
+ attr_reader :commits
+ attr_reader :ref
+ attr_reader :ref_type
+
+ def initialize(params)
+ super
+
+ @after = params[:after]
+ @before = params[:before]
+ @commits = params.fetch(:commits, [])
+ @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
+ @ref = Gitlab::Git.ref_name(params[:ref])
+ end
+
+ def attachments
+ return [] if new_branch? || removed_branch?
+ return commit_messages if markdown
+
+ commit_message_attachments
+ end
+
+ def activity
+ {
+ title: humanized_action(short: true),
+ subtitle: "in #{project_link}",
+ text: compare_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def humanized_action(short: false)
+ action, ref_link, target_link = compose_action_details
+ text = [user_combined_name, action, ref_type, ref_link]
+ text << target_link unless short
+ text.join(' ')
+ end
+
+ def message
+ humanized_action
+ end
+
+ def format(string)
+ Slack::Messenger::Util::LinkFormatter.format(string)
+ end
+
+ def commit_messages
+ commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
+ end
+
+ def commit_message_attachments
+ [{ text: format(commit_messages), color: attachment_color }]
+ end
+
+ def compose_commit_message(commit)
+ author = commit[:author][:name]
+ id = Commit.truncate_sha(commit[:id])
+ title = commit[:title]
+
+ url = commit[:url]
+
+ "[#{id}](#{url}): #{title} - #{author}"
+ end
+
+ def new_branch?
+ Gitlab::Git.blank_ref?(before)
+ end
+
+ def removed_branch?
+ Gitlab::Git.blank_ref?(after)
+ end
+
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/commits/#{ref}"
+ end
+ end
+
+ def compare_url
+ "#{project_url}/compare/#{before}...#{after}"
+ end
+
+ def ref_link
+ "[#{ref}](#{ref_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def compare_link
+ "[Compare changes](#{compare_url})"
+ end
+
+ def compose_action_details
+ if new_branch?
+ ['pushed new', ref_link, "to #{project_link}"]
+ elsif removed_branch?
+ ['removed', ref, "from #{project_link}"]
+ else
+ ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
+ end
+ end
+
+ def attachment_color
+ '#345'
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/wiki_page_message.rb b/app/models/integrations/chat_message/wiki_page_message.rb
new file mode 100644
index 00000000000..9b5275b8c03
--- /dev/null
+++ b/app/models/integrations/chat_message/wiki_page_message.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class WikiPageMessage < BaseMessage
+ attr_reader :title
+ attr_reader :wiki_page_url
+ attr_reader :action
+ attr_reader :description
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @wiki_page_url = obj_attr[:url]
+ @description = obj_attr[:message]
+
+ @action =
+ case obj_attr[:action]
+ when "create"
+ "created"
+ when "update"
+ "edited"
+ end
+ end
+
+ def attachments
+ return description if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "#{user_combined_name} #{action} #{wiki_page_link}",
+ subtitle: "in #{project_link}",
+ text: title,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ end
+
+ def description_message
+ [{ text: format(@description), color: attachment_color }]
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def wiki_page_link
+ "[wiki page](#{wiki_page_url})"
+ end
+ end
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index fa7921afcbf..36edf646658 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -117,11 +117,6 @@ class Packages::Package < ApplicationRecord
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
scope :has_version, -> { where.not(version: nil) }
- scope :processed, -> do
- where.not(package_type: :nuget).or(
- where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME)
- )
- end
scope :preload_files, -> { preload(:package_files) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index bd4b232849f..17131cd736d 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -62,17 +62,16 @@ module Pages
}
end
+ # TODO: remove support for legacy storage in 14.3 https://gitlab.com/gitlab-org/gitlab/-/issues/328712
+ # we support this till 14.3 to allow people to still use legacy storage if something goes very wrong
+ # on self-hosted installations, and we'll need some time to fix it
def legacy_source
- raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true)
+ return unless ::Settings.pages.local_store.enabled
{
type: 'file',
path: File.join(project.full_path, 'public/')
}
- rescue LegacyStorageDisabledError => e
- Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
-
- nil
end
end
end
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 90cb8253b52..497f67993ae 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -21,9 +21,7 @@ module Pages
project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain)
end
- # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524
- # source can only be nil if pages_serve_from_legacy_storage FF is disabled
- # we can remove this filtering once we remove legacy storage
+ # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715
paths = paths.select(&:source)
paths.sort_by(&:prefix).reverse
diff --git a/app/models/project_services/chat_message/alert_message.rb b/app/models/project_services/chat_message/alert_message.rb
deleted file mode 100644
index c8913775843..00000000000
--- a/app/models/project_services/chat_message/alert_message.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class AlertMessage < BaseMessage
- attr_reader :title
- attr_reader :alert_url
- attr_reader :severity
- attr_reader :events
- attr_reader :status
- attr_reader :started_at
-
- def initialize(params)
- @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
- @project_url = params.dig(:project, :web_url) || params[:project_url]
- @title = params.dig(:object_attributes, :title)
- @alert_url = params.dig(:object_attributes, :url)
- @severity = params.dig(:object_attributes, :severity)
- @events = params.dig(:object_attributes, :events)
- @status = params.dig(:object_attributes, :status)
- @started_at = params.dig(:object_attributes, :started_at)
- end
-
- def attachments
- [{
- title: title,
- title_link: alert_url,
- color: attachment_color,
- fields: attachment_fields
- }]
- end
-
- def message
- "Alert firing in #{project_name}"
- end
-
- private
-
- def attachment_color
- "#C95823"
- end
-
- def attachment_fields
- [
- {
- title: "Severity",
- value: severity.to_s.humanize,
- short: true
- },
- {
- title: "Events",
- value: events,
- short: true
- },
- {
- title: "Status",
- value: status.to_s.humanize,
- short: true
- },
- {
- title: "Start time",
- value: format_time(started_at),
- short: true
- }
- ]
- end
-
- # This formats time into the following format
- # April 23rd, 2020 1:06AM UTC
- def format_time(time)
- time = Time.zone.parse(time.to_s)
- time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
- end
- end
-end
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
deleted file mode 100644
index bdd77a919e3..00000000000
--- a/app/models/project_services/chat_message/base_message.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class BaseMessage
- RELATIVE_LINK_REGEX = /!\[[^\]]*\]\((\/uploads\/[^\)]*)\)/.freeze
-
- attr_reader :markdown
- attr_reader :user_full_name
- attr_reader :user_name
- attr_reader :user_avatar
- attr_reader :project_name
- attr_reader :project_url
-
- def initialize(params)
- @markdown = params[:markdown] || false
- @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
- @project_url = params.dig(:project, :web_url) || params[:project_url]
- @user_full_name = params.dig(:user, :name) || params[:user_full_name]
- @user_name = params.dig(:user, :username) || params[:user_name]
- @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
- end
-
- def user_combined_name
- if user_full_name.present?
- "#{user_full_name} (#{user_name})"
- else
- user_name
- end
- end
-
- def summary
- return message if markdown
-
- format(message)
- end
-
- def pretext
- summary
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- raise NotImplementedError
- end
-
- def activity
- raise NotImplementedError
- end
-
- private
-
- def message
- raise NotImplementedError
- end
-
- def format(string)
- Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
- end
-
- def format_relative_links(string)
- string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
- end
-
- def attachment_color
- '#345'
- end
-
- def link(text, url)
- "[#{text}](#{url})"
- end
-
- def pretty_duration(seconds)
- parse_string =
- if duration < 1.hour
- '%M:%S'
- else
- '%H:%M:%S'
- end
-
- Time.at(seconds).utc.strftime(parse_string)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb
deleted file mode 100644
index 5deb757e60f..00000000000
--- a/app/models/project_services/chat_message/deployment_message.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class DeploymentMessage < BaseMessage
- attr_reader :commit_title
- attr_reader :commit_url
- attr_reader :deployable_id
- attr_reader :deployable_url
- attr_reader :environment
- attr_reader :short_sha
- attr_reader :status
- attr_reader :user_url
-
- def initialize(data)
- super
-
- @commit_title = data[:commit_title]
- @commit_url = data[:commit_url]
- @deployable_id = data[:deployable_id]
- @deployable_url = data[:deployable_url]
- @environment = data[:environment]
- @short_sha = data[:short_sha]
- @status = data[:status]
- @user_url = data[:user_url]
- end
-
- def attachments
- [{
- text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
- color: color
- }]
- end
-
- def activity
- {}
- end
-
- private
-
- def message
- if running?
- "Starting deploy to #{environment}"
- else
- "Deploy to #{environment} #{humanized_status}"
- end
- end
-
- def color
- case status
- when 'success'
- 'good'
- when 'canceled'
- 'warning'
- when 'failed'
- 'danger'
- else
- '#334455'
- end
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def deployment_link
- link("##{deployable_id}", deployable_url)
- end
-
- def user_link
- link(user_combined_name, user_url)
- end
-
- def commit_link
- link(short_sha, commit_url)
- end
-
- def humanized_status
- status == 'success' ? 'succeeded' : status
- end
-
- def running?
- status == 'running'
- end
- end
-end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
deleted file mode 100644
index c8e90b66bae..00000000000
--- a/app/models/project_services/chat_message/issue_message.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class IssueMessage < BaseMessage
- attr_reader :title
- attr_reader :issue_iid
- attr_reader :issue_url
- attr_reader :action
- attr_reader :state
- attr_reader :description
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @issue_iid = obj_attr[:iid]
- @issue_url = obj_attr[:url]
- @action = obj_attr[:action]
- @state = obj_attr[:state]
- @description = obj_attr[:description] || ''
- end
-
- def attachments
- return [] unless opened_issue?
- return description if markdown
-
- description_message
- end
-
- def activity
- {
- title: "Issue #{state} by #{user_combined_name}",
- subtitle: "in #{project_link}",
- text: issue_link,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
- end
-
- def opened_issue?
- action == 'open'
- end
-
- def description_message
- [{
- title: issue_title,
- title_link: issue_url,
- text: format(description),
- color: '#C95823'
- }]
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def issue_link
- link(issue_title, issue_url)
- end
-
- def issue_title
- "#{Issue.reference_prefix}#{issue_iid} #{title}"
- end
- end
-end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
deleted file mode 100644
index e45bb9b8ce1..00000000000
--- a/app/models/project_services/chat_message/merge_message.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class MergeMessage < BaseMessage
- attr_reader :merge_request_iid
- attr_reader :source_branch
- attr_reader :target_branch
- attr_reader :action
- attr_reader :state
- attr_reader :title
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @merge_request_iid = obj_attr[:iid]
- @source_branch = obj_attr[:source_branch]
- @target_branch = obj_attr[:target_branch]
- @action = obj_attr[:action]
- @state = obj_attr[:state]
- @title = format_title(obj_attr[:title])
- end
-
- def attachments
- []
- end
-
- def activity
- {
- title: "Merge request #{state_or_action_text} by #{user_combined_name}",
- subtitle: "in #{project_link}",
- text: merge_request_link,
- image: user_avatar
- }
- end
-
- private
-
- def format_title(title)
- '*' + title.lines.first.chomp + '*'
- end
-
- def message
- merge_request_message
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def merge_request_message
- "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
- end
-
- def merge_request_link
- link(merge_request_title, merge_request_url)
- end
-
- def merge_request_title
- "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
- end
-
- def merge_request_url
- "#{project_url}/-/merge_requests/#{merge_request_iid}"
- end
-
- def state_or_action_text
- case action
- when 'approved', 'unapproved'
- action
- when 'approval'
- 'added their approval to'
- when 'unapproval'
- 'removed their approval from'
- else
- state
- end
- end
- end
-end
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
deleted file mode 100644
index 741474fb27b..00000000000
--- a/app/models/project_services/chat_message/note_message.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class NoteMessage < BaseMessage
- attr_reader :note
- attr_reader :note_url
- attr_reader :title
- attr_reader :target
-
- def initialize(params)
- super
-
- params = HashWithIndifferentAccess.new(params)
- obj_attr = params[:object_attributes]
- @note = obj_attr[:note]
- @note_url = obj_attr[:url]
- @target, @title = case obj_attr[:noteable_type]
- when "Commit"
- create_commit_note(params[:commit])
- when "Issue"
- create_issue_note(params[:issue])
- when "MergeRequest"
- create_merge_note(params[:merge_request])
- when "Snippet"
- create_snippet_note(params[:snippet])
- end
- end
-
- def attachments
- return note if markdown
-
- description_message
- end
-
- def activity
- {
- title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
- subtitle: "in #{project_link}",
- text: formatted_title,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
- end
-
- def format_title(title)
- title.lines.first.chomp
- end
-
- def formatted_title
- format_title(title)
- end
-
- def create_issue_note(issue)
- ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
- end
-
- def create_commit_note(commit)
- commit_sha = Commit.truncate_sha(commit[:id])
-
- ["commit #{commit_sha}", commit[:message]]
- end
-
- def create_merge_note(merge_request)
- ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
- end
-
- def create_snippet_note(snippet)
- ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
- end
-
- def description_message
- [{ text: format(note), color: attachment_color }]
- end
-
- def project_link
- link(project_name, project_url)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
deleted file mode 100644
index f4c6938fa78..00000000000
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ /dev/null
@@ -1,265 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class PipelineMessage < BaseMessage
- MAX_VISIBLE_JOBS = 10
-
- attr_reader :user
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :detailed_status
- attr_reader :duration
- attr_reader :finished_at
- attr_reader :pipeline_id
- attr_reader :failed_stages
- attr_reader :failed_jobs
-
- attr_reader :project
- attr_reader :commit
- attr_reader :committer
- attr_reader :pipeline
-
- def initialize(data)
- super
-
- @user = data[:user]
- @user_name = data.dig(:user, :username) || 'API'
-
- pipeline_attributes = data[:object_attributes]
- @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
- @ref = pipeline_attributes[:ref]
- @status = pipeline_attributes[:status]
- @detailed_status = pipeline_attributes[:detailed_status]
- @duration = pipeline_attributes[:duration].to_i
- @finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
- @pipeline_id = pipeline_attributes[:id]
-
- # Get list of jobs that have actually failed (after exhausting all retries)
- @failed_jobs = actually_failed_jobs(Array(data[:builds]))
- @failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
-
- @project = Project.find(data[:project][:id])
- @commit = project.commit_by(oid: data[:commit][:id])
- @committer = commit.committer
- @pipeline = Ci::Pipeline.find(pipeline_id)
- end
-
- def pretext
- ''
- end
-
- def attachments
- return message if markdown
-
- [{
- fallback: format(message),
- color: attachment_color,
- author_name: user_combined_name,
- author_icon: user_avatar,
- author_link: author_url,
- title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
- {
- pipeline_id: pipeline_id,
- humanized_status: humanized_status,
- duration: pretty_duration(duration)
- },
- title_link: pipeline_url,
- fields: attachments_fields,
- footer: project.name,
- footer_icon: project.avatar_url(only_path: false),
- ts: finished_at
- }]
- end
-
- def activity
- {
- title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
- {
- pipeline_link: pipeline_link,
- ref_type: ref_type,
- ref_link: ref_link,
- user_combined_name: user_combined_name,
- humanized_status: humanized_status
- },
- subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
- text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
- image: user_avatar || ''
- }
- end
-
- private
-
- def actually_failed_jobs(builds)
- succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
-
- failed_jobs = builds.select do |build|
- # Select jobs which doesn't have a successful retry
- build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
- end
-
- failed_jobs.uniq { |job| job[:name] }.reverse
- end
-
- def failed_stages_field
- {
- title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
- short: true
- }
- end
-
- def failed_jobs_field
- {
- title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
- short: true
- }
- end
-
- def yaml_error_field
- {
- title: s_("ChatMessage|Invalid CI config YAML file"),
- value: pipeline.yaml_errors,
- short: false
- }
- end
-
- def attachments_fields
- fields = [
- {
- title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
- value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
- short: true
- },
- {
- title: s_("ChatMessage|Commit"),
- value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
- short: true
- }
- ]
-
- fields << failed_stages_field if failed_stages.any?
- fields << failed_jobs_field if failed_jobs.any?
- fields << yaml_error_field if pipeline.has_yaml_errors?
-
- fields
- end
-
- def message
- s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
- {
- project_link: project_link,
- pipeline_link: pipeline_link,
- ref_type: ref_type,
- ref_link: ref_link,
- user_combined_name: user_combined_name,
- humanized_status: humanized_status,
- duration: pretty_duration(duration)
- }
- end
-
- def humanized_status
- case status
- when 'success'
- detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
- when 'failed'
- s_("ChatMessage|has failed")
- else
- status
- end
- end
-
- def attachment_color
- case status
- when 'success'
- detailed_status == 'passed with warnings' ? 'warning' : 'good'
- else
- 'danger'
- end
- end
-
- def ref_url
- if ref_type == 'tag'
- "#{project_url}/-/tags/#{ref}"
- else
- "#{project_url}/-/commits/#{ref}"
- end
- end
-
- def ref_link
- "[#{ref}](#{ref_url})"
- end
-
- def project_url
- project.web_url
- end
-
- def project_link
- "[#{project.name}](#{project_url})"
- end
-
- def pipeline_failed_jobs_url
- "#{project_url}/-/pipelines/#{pipeline_id}/failures"
- end
-
- def pipeline_url
- if failed_jobs.any?
- pipeline_failed_jobs_url
- else
- "#{project_url}/-/pipelines/#{pipeline_id}"
- end
- end
-
- def pipeline_link
- "[##{pipeline_id}](#{pipeline_url})"
- end
-
- def job_url(job)
- "#{project_url}/-/jobs/#{job[:id]}"
- end
-
- def job_link(job)
- "[#{job[:name]}](#{job_url(job)})"
- end
-
- def failed_jobs_links
- failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
- truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
-
- failed_links = failed.map { |job| job_link(job) }
-
- unless truncated.blank?
- failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
- count: truncated.size,
- pipeline_failed_jobs_url: pipeline_failed_jobs_url
- }
- end
-
- failed_links.join(I18n.translate(:'support.array.words_connector'))
- end
-
- def stage_link(stage)
- # All stages link to the pipeline page
- "[#{stage}](#{pipeline_url})"
- end
-
- def failed_stages_links
- failed_stages.map { |s| stage_link(s) }.join(I18n.translate(:'support.array.words_connector'))
- end
-
- def commit_url
- Gitlab::UrlBuilder.build(commit)
- end
-
- def commit_link
- "[#{commit.title}](#{commit_url})"
- end
-
- def author_url
- return unless user && committer
-
- Gitlab::UrlBuilder.build(committer)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
deleted file mode 100644
index c8e70a69c88..00000000000
--- a/app/models/project_services/chat_message/push_message.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class PushMessage < BaseMessage
- attr_reader :after
- attr_reader :before
- attr_reader :commits
- attr_reader :ref
- attr_reader :ref_type
-
- def initialize(params)
- super
-
- @after = params[:after]
- @before = params[:before]
- @commits = params.fetch(:commits, [])
- @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
- @ref = Gitlab::Git.ref_name(params[:ref])
- end
-
- def attachments
- return [] if new_branch? || removed_branch?
- return commit_messages if markdown
-
- commit_message_attachments
- end
-
- def activity
- {
- title: humanized_action(short: true),
- subtitle: "in #{project_link}",
- text: compare_link,
- image: user_avatar
- }
- end
-
- private
-
- def humanized_action(short: false)
- action, ref_link, target_link = compose_action_details
- text = [user_combined_name, action, ref_type, ref_link]
- text << target_link unless short
- text.join(' ')
- end
-
- def message
- humanized_action
- end
-
- def format(string)
- Slack::Messenger::Util::LinkFormatter.format(string)
- end
-
- def commit_messages
- commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
- end
-
- def commit_message_attachments
- [{ text: format(commit_messages), color: attachment_color }]
- end
-
- def compose_commit_message(commit)
- author = commit[:author][:name]
- id = Commit.truncate_sha(commit[:id])
- title = commit[:title]
-
- url = commit[:url]
-
- "[#{id}](#{url}): #{title} - #{author}"
- end
-
- def new_branch?
- Gitlab::Git.blank_ref?(before)
- end
-
- def removed_branch?
- Gitlab::Git.blank_ref?(after)
- end
-
- def ref_url
- if ref_type == 'tag'
- "#{project_url}/-/tags/#{ref}"
- else
- "#{project_url}/commits/#{ref}"
- end
- end
-
- def compare_url
- "#{project_url}/compare/#{before}...#{after}"
- end
-
- def ref_link
- "[#{ref}](#{ref_url})"
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def compare_link
- "[Compare changes](#{compare_url})"
- end
-
- def compose_action_details
- if new_branch?
- ['pushed new', ref_link, "to #{project_link}"]
- elsif removed_branch?
- ['removed', ref, "from #{project_link}"]
- else
- ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
- end
- end
-
- def attachment_color
- '#345'
- end
- end
-end
diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
deleted file mode 100644
index ebe7abb379f..00000000000
--- a/app/models/project_services/chat_message/wiki_page_message.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class WikiPageMessage < BaseMessage
- attr_reader :title
- attr_reader :wiki_page_url
- attr_reader :action
- attr_reader :description
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @wiki_page_url = obj_attr[:url]
- @description = obj_attr[:message]
-
- @action =
- case obj_attr[:action]
- when "create"
- "created"
- when "update"
- "edited"
- end
- end
-
- def attachments
- return description if markdown
-
- description_message
- end
-
- def activity
- {
- title: "#{user_combined_name} #{action} #{wiki_page_link}",
- subtitle: "in #{project_link}",
- text: title,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
- end
-
- def description_message
- [{ text: format(@description), color: attachment_color }]
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def wiki_page_link
- "[wiki page](#{wiki_page_url})"
- end
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index c95a5d4e6cb..2f841bf903e 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -185,19 +185,19 @@ class ChatNotificationService < Integration
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
- ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
+ Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue"
- ChatMessage::IssueMessage.new(data) unless update?(data)
+ Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
- ChatMessage::MergeMessage.new(data) unless update?(data)
+ Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
- ChatMessage::NoteMessage.new(data)
+ Integrations::ChatMessage::NoteMessage.new(data)
when "pipeline"
- ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
- ChatMessage::WikiPageMessage.new(data)
+ Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
- ChatMessage::DeploymentMessage.new(data)
+ Integrations::ChatMessage::DeploymentMessage.new(data)
end
end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 7badcc24870..92a46f8d01f 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -39,7 +39,7 @@ class SlackService < ChatNotificationService
end
def get_message(object_kind, data)
- return ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
+ return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
super
end
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
index 8b5a94c021c..b75905fb5b0 100644
--- a/app/services/issuable/destroy_service.rb
+++ b/app/services/issuable/destroy_service.rb
@@ -26,25 +26,13 @@ module Issuable
end
def delete_todos(actor, issuable)
- if Feature.enabled?(:destroy_issuable_todos_async, actor, default_enabled: :yaml)
- TodosDestroyer::DestroyedIssuableWorker
- .perform_async(issuable.id, issuable.class.name)
- else
- TodosDestroyer::DestroyedIssuableWorker
- .new
- .perform(issuable.id, issuable.class.name)
- end
+ TodosDestroyer::DestroyedIssuableWorker
+ .perform_async(issuable.id, issuable.class.name)
end
def delete_label_links(actor, issuable)
- if Feature.enabled?(:destroy_issuable_label_links_async, actor, default_enabled: :yaml)
- Issuable::LabelLinksDestroyWorker
- .perform_async(issuable.id, issuable.class.name)
- else
- Issuable::LabelLinksDestroyWorker
- .new
- .perform(issuable.id, issuable.class.name)
- end
+ Issuable::LabelLinksDestroyWorker
+ .perform_async(issuable.id, issuable.class.name)
end
end
end
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
index 32e58fcc06b..7155017b73f 100644
--- a/app/services/spam/spam_verdict_service.rb
+++ b/app/services/spam/spam_verdict_service.rb
@@ -16,12 +16,17 @@ module Spam
def execute
spamcheck_result = nil
spamcheck_attribs = {}
+ spamcheck_error = false
external_spam_check_round_trip_time = Benchmark.realtime do
- spamcheck_result, spamcheck_attribs = spamcheck_verdict
+ spamcheck_result, spamcheck_attribs, spamcheck_error = spamcheck_verdict
end
- # assign result to a var and log it before reassigning to nil when monitorMode is true
+ label = spamcheck_error ? 'ERROR' : spamcheck_result.to_s.upcase
+
+ histogram.observe( { result: label }, external_spam_check_round_trip_time )
+
+ # assign result to a var for logging it before reassigning to nil when monitorMode is true
original_spamcheck_result = spamcheck_result
spamcheck_result = nil if spamcheck_attribs&.fetch("monitorMode", "false") == "true"
@@ -83,8 +88,9 @@ module Spam
end
rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e)
+
# Default to ALLOW if any errors occur
- [ALLOW, attribs]
+ [ALLOW, attribs, true]
end
end
@@ -95,5 +101,9 @@ module Spam
def logger
@logger ||= Gitlab::AppJsonLogger.build
end
+
+ def histogram
+ @histogram ||= Gitlab::Metrics.histogram(:gitlab_spamcheck_request_duration_seconds, 'Request duration to the anti-spam service')
+ end
end
end
diff --git a/app/views/notify/change_in_merge_request_draft_status_email.html.haml b/app/views/notify/change_in_merge_request_draft_status_email.html.haml
index 5604a30d9f1..64ceb77e85c 100644
--- a/app/views/notify/change_in_merge_request_draft_status_email.html.haml
+++ b/app/views/notify/change_in_merge_request_draft_status_email.html.haml
@@ -1,2 +1,2 @@
-%p
- = _('%{username} changed the draft status of merge request %{mr_reference}' % {username: sanitize_name(@updated_by_user.name), mr_reference: @merge_request.to_reference })
+%p= html_escape(_('%{username} changed the draft status of merge request %{mr_link}')) % { username: link_to(@updated_by_user.name, user_url(@updated_by_user)),
+ mr_link: merge_request_reference_link(@merge_request) }
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 6c3e15cbace..01ab7bf9cd4 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -6,7 +6,7 @@
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0
- %li.issuable-mr.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests') }
+ %li.issuable-mr.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests'), data: { testid: 'merge-requests' } }
= sprite_icon('merge-request', css_class: "gl-vertical-align-middle")
= issuable_mr
diff --git a/changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml b/changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml
new file mode 100644
index 00000000000..b8acba79b42
--- /dev/null
+++ b/changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml
@@ -0,0 +1,5 @@
+---
+title: Change wording for design management upload
+merge_request: 61782
+author:
+type: other
diff --git a/changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml b/changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml
new file mode 100644
index 00000000000..678b65a15c7
--- /dev/null
+++ b/changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml
@@ -0,0 +1,5 @@
+---
+title: Remove pages_update_legacy_storage feature flag
+merge_request: 60005
+author:
+type: added
diff --git a/changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml b/changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml
new file mode 100644
index 00000000000..44959cfb408
--- /dev/null
+++ b/changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml
@@ -0,0 +1,5 @@
+---
+title: Remove pages_serve_from_legacy_storage feature flag
+merge_request: 60010
+author:
+type: added
diff --git a/changelogs/unreleased/325689-remove-delete-async-ffs.yml b/changelogs/unreleased/325689-remove-delete-async-ffs.yml
new file mode 100644
index 00000000000..fcccf464740
--- /dev/null
+++ b/changelogs/unreleased/325689-remove-delete-async-ffs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove issuable destroy service related FFs
+merge_request: 61764
+author:
+type: other
diff --git a/changelogs/unreleased/329521-store-segment-target-groups.yml b/changelogs/unreleased/329521-store-segment-target-groups.yml
new file mode 100644
index 00000000000..d59585c5fce
--- /dev/null
+++ b/changelogs/unreleased/329521-store-segment-target-groups.yml
@@ -0,0 +1,5 @@
+---
+title: Prepare devops adoption database structure for migration
+merge_request: 60733
+author:
+type: other
diff --git a/changelogs/unreleased/330845-observe-limit-to-hours.yml b/changelogs/unreleased/330845-observe-limit-to-hours.yml
new file mode 100644
index 00000000000..c6108df10f0
--- /dev/null
+++ b/changelogs/unreleased/330845-observe-limit-to-hours.yml
@@ -0,0 +1,5 @@
+---
+title: Observe limit to hours setting in timelog report
+merge_request: 61849
+author: Lee Tickett @leetickett
+type: added
diff --git a/changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml b/changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml
new file mode 100644
index 00000000000..06d4c086735
--- /dev/null
+++ b/changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml
@@ -0,0 +1,5 @@
+---
+title: Add duplicated gin trigram index on notes table (note) to replace existing
+merge_request: 61430
+author:
+type: performance
diff --git a/changelogs/unreleased/cablett-change-draft-status-email.yml b/changelogs/unreleased/cablett-change-draft-status-email.yml
new file mode 100644
index 00000000000..5d364a240c6
--- /dev/null
+++ b/changelogs/unreleased/cablett-change-draft-status-email.yml
@@ -0,0 +1,5 @@
+---
+title: Add link to email notifying of MR changing draft status
+merge_request: 61891
+author:
+type: changed
diff --git a/changelogs/unreleased/grape-action-caching-default.yml b/changelogs/unreleased/grape-action-caching-default.yml
new file mode 100644
index 00000000000..de46a8e75f6
--- /dev/null
+++ b/changelogs/unreleased/grape-action-caching-default.yml
@@ -0,0 +1,5 @@
+---
+title: Apply rate-limit cache to branches endpoint
+merge_request: 61723
+author:
+type: performance
diff --git a/changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml b/changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml
new file mode 100644
index 00000000000..e2300a390e2
--- /dev/null
+++ b/changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml
@@ -0,0 +1,5 @@
+---
+title: Send in-product marketing email usage data
+merge_request: 56752
+author:
+type: changed
diff --git a/config/feature_flags/development/api_caching_rate_limit_branches.yml b/config/feature_flags/development/api_caching_rate_limit_branches.yml
index a4feb9fc8a0..a48e4660342 100644
--- a/config/feature_flags/development/api_caching_rate_limit_branches.yml
+++ b/config/feature_flags/development/api_caching_rate_limit_branches.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330876
milestone: '13.12'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/destroy_issuable_label_links_async.yml b/config/feature_flags/development/destroy_issuable_label_links_async.yml
deleted file mode 100644
index 14abd9459f9..00000000000
--- a/config/feature_flags/development/destroy_issuable_label_links_async.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: destroy_issuable_label_links_async
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60487
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325689
-milestone: '13.12'
-type: development
-group: group::code review
-default_enabled: false
diff --git a/config/feature_flags/development/destroy_issuable_todos_async.yml b/config/feature_flags/development/destroy_issuable_todos_async.yml
deleted file mode 100644
index c39e551bdd9..00000000000
--- a/config/feature_flags/development/destroy_issuable_todos_async.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: destroy_issuable_todos_async
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57830
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325689
-milestone: '13.11'
-type: development
-group: group::code review
-default_enabled: false
diff --git a/config/feature_flags/development/pages_serve_from_legacy_storage.yml b/config/feature_flags/development/pages_serve_from_legacy_storage.yml
deleted file mode 100644
index 37d83106737..00000000000
--- a/config/feature_flags/development/pages_serve_from_legacy_storage.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: pages_serve_from_legacy_storage
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297228
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297524
-milestone: '13.8'
-type: development
-group: group::release
-default_enabled: true
diff --git a/config/feature_flags/development/pages_update_legacy_storage.yml b/config/feature_flags/development/pages_update_legacy_storage.yml
deleted file mode 100644
index 4a228b4cb8b..00000000000
--- a/config/feature_flags/development/pages_update_legacy_storage.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: pages_update_legacy_storage
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50683
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296138
-milestone: '13.9'
-type: development
-group: group::release
-default_enabled: true
diff --git a/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml b/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml
new file mode 100644
index 00000000000..bbfae3eb114
--- /dev/null
+++ b/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_create_0_sent
+name: "count_sent_first_email_of_the_create_track_for_in_product_marketing_emails"
+description: Total sent emails of the create track's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml b/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml
new file mode 100644
index 00000000000..a2cf7c6d813
--- /dev/null
+++ b/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.in_product_marketing_email_create_0_cta_clicked
+name: "count_clicks_on_the_first_email_of_the_create_track_for_in_product_marketing_emails"
+description: Total clicks on the create track's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+
diff --git a/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml b/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml
new file mode 100644
index 00000000000..85552f42431
--- /dev/null
+++ b/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_create_1_sent
+name: "count_sent_second_email_of_the_create_track_for_in_product_marketing_emails"
+description: Total sent emails of the create track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml b/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml
new file mode 100644
index 00000000000..7a1e5f28c22
--- /dev/null
+++ b/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.in_product_marketing_email_create_1_cta_clicked
+name: "count_clicks_on_the_second_email_of_the_create_track_for_in_product_marketing_emails"
+description: Total clicks on the create track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+
diff --git a/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml b/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml
new file mode 100644
index 00000000000..02d5ae115bb
--- /dev/null
+++ b/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_create_2_sent
+name: "count_sent_third_email_of_the_create_track_for_in_product_marketing_emails"
+description: Total sent emails of the create track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml b/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml
new file mode 100644
index 00000000000..75eafa3a54f
--- /dev/null
+++ b/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_create_2_cta_clicked
+name: "count_clicks_on_the_third_email_of_the_create_track_for_in_product_marketing_emails"
+description: Total clicks on the create track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml b/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml
new file mode 100644
index 00000000000..0a5a9ef936f
--- /dev/null
+++ b/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_verify_0_sent
+name: "count_sent_first_email_of_the_verify_track_for_in_product_marketing_emails"
+description: Total sent emails of the verify track's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml b/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml
new file mode 100644
index 00000000000..17e677ba29d
--- /dev/null
+++ b/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_verify_0_cta_clicked
+name: "count_clicks_on_the_first_email_of_the_verify_track_for_in_product_marketing_emails"
+description: Total clicks on the verify track's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml b/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml
new file mode 100644
index 00000000000..2bad6d31db2
--- /dev/null
+++ b/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_verify_1_sent
+name: "count_sent_second_email_of_the_verify_track_for_in_product_marketing_emails"
+description: Total sent emails of the verify track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml b/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml
new file mode 100644
index 00000000000..66080d46e7f
--- /dev/null
+++ b/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.in_product_marketing_email_verify_1_cta_clicked
+name: "count_clicks_on_the_second_email_of_the_verify_track_for_in_product_marketing_emails"
+description: Total clicks on the verify track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+
diff --git a/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml b/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml
new file mode 100644
index 00000000000..2beabb8b007
--- /dev/null
+++ b/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_verify_2_sent
+name: "count_sent_third_email_of_the_verify_track_for_in_product_marketing_emails"
+description: Total sent emails of the verify track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml b/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml
new file mode 100644
index 00000000000..7213c463fa9
--- /dev/null
+++ b/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_verify_2_cta_clicked
+name: "count_clicks_on_the_third_email_of_the_verify_track_for_in_product_marketing_emails"
+description: Total clicks on the verify track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml b/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml
new file mode 100644
index 00000000000..dc566f03898
--- /dev/null
+++ b/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_trial_0_sent
+name: "count_sent_first_email_of_the_trial_track_for_in_product_marketing_emails"
+description: Total sent emails of the trial track's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml b/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml
new file mode 100644
index 00000000000..a3cf714e5ad
--- /dev/null
+++ b/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_trial_0_cta_clicked
+name: "count_clicks_on_the_first_email_of_the_trial_track_for_in_product_marketing_emails"
+description: Total clicks on the verify trial's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml b/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml
new file mode 100644
index 00000000000..f5215090b7e
--- /dev/null
+++ b/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_trial_1_sent
+name: "count_sent_second_email_of_the_trial_track_for_in_product_marketing_emails"
+description: Total sent emails of the trial track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml b/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml
new file mode 100644
index 00000000000..651c5426e2a
--- /dev/null
+++ b/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.in_product_marketing_email_trial_1_cta_clicked
+name: "count_clicks_on_the_second_email_of_the_trial_track_for_in_product_marketing_emails"
+description: Total clicks on the trial track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+
diff --git a/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml b/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml
new file mode 100644
index 00000000000..4be98d45ce2
--- /dev/null
+++ b/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_trial_2_sent
+name: "count_sent_third_email_of_the_trial_track_for_in_product_marketing_emails"
+description: Total sent emails of the trial track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml b/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml
new file mode 100644
index 00000000000..6be928da906
--- /dev/null
+++ b/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_trial_2_cta_clicked
+name: "count_clicks_on_the_third_email_of_the_trial_track_for_in_product_marketing_emails"
+description: Total clicks on the trial track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml b/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml
new file mode 100644
index 00000000000..ac9ffa730f1
--- /dev/null
+++ b/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_team_0_sent
+name: "count_sent_first_email_of_the_trial_team_for_in_product_marketing_emails"
+description: Total sent emails of the team track's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml b/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml
new file mode 100644
index 00000000000..cf51512c6eb
--- /dev/null
+++ b/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_team_0_cta_clicked
+name: "count_clicks_on_the_first_email_of_the_team_track_for_in_product_marketing_emails"
+description: Total clicks on the team track's first email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml b/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml
new file mode 100644
index 00000000000..b860b08e391
--- /dev/null
+++ b/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_team_1_sent
+name: "count_sent_second_email_of_the_team_track_for_in_product_marketing_emails"
+description: Total sent emails of the team track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml b/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml
new file mode 100644
index 00000000000..c0f63cadbf2
--- /dev/null
+++ b/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.in_product_marketing_email_team_1_cta_clicked
+name: "count_clicks_on_the_second_email_of_the_team_track_for_in_product_marketing_emails"
+description: Total clicks on the team track's second email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+
diff --git a/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml b/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml
new file mode 100644
index 00000000000..887334c65c7
--- /dev/null
+++ b/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_team_2_sent
+name: "count_sent_third_email_of_the_team_track_for_in_product_marketing_emails"
+description: Total sent emails of the team track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml b/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml
new file mode 100644
index 00000000000..6dac2db454a
--- /dev/null
+++ b/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml
@@ -0,0 +1,21 @@
+---
+key_path: counts.in_product_marketing_email_team_2_cta_clicked
+name: "count_clicks_on_the_third_email_of_the_team_track_for_in_product_marketing_emails"
+description: Total clicks on the team track's third email
+product_section:
+product_stage: growth
+product_group: group::activation
+product_category: onboarding
+value_type: number
+status: implemented
+milestone: "13.12"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/db/migrate/20210430122951_add_snapshot_namespace_id.rb b/db/migrate/20210430122951_add_snapshot_namespace_id.rb
new file mode 100644
index 00000000000..9017bcdde53
--- /dev/null
+++ b/db/migrate/20210430122951_add_snapshot_namespace_id.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddSnapshotNamespaceId < ActiveRecord::Migration[6.0]
+ def change
+ add_column :analytics_devops_adoption_snapshots, :namespace_id, :integer
+ end
+end
diff --git a/db/migrate/20210430124212_add_display_namespace_id_to_segments.rb b/db/migrate/20210430124212_add_display_namespace_id_to_segments.rb
new file mode 100644
index 00000000000..43be5c719fb
--- /dev/null
+++ b/db/migrate/20210430124212_add_display_namespace_id_to_segments.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddDisplayNamespaceIdToSegments < ActiveRecord::Migration[6.0]
+ def change
+ add_column :analytics_devops_adoption_segments, :display_namespace_id, :integer
+ end
+end
diff --git a/db/migrate/20210430124630_add_devops_adoption_indexes.rb b/db/migrate/20210430124630_add_devops_adoption_indexes.rb
new file mode 100644
index 00000000000..4531e6b5e4c
--- /dev/null
+++ b/db/migrate/20210430124630_add_devops_adoption_indexes.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class AddDevopsAdoptionIndexes < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ SEGMENTS_INDEX_NAME = 'idx_devops_adoption_segments_namespaces_pair'
+ SNAPSHOT_END_TIME_INDEX_NAME = 'idx_devops_adoption_segments_namespace_end_time'
+ SNAPSHOT_RECORDED_AT_INDEX_NAME = 'idx_devops_adoption_segments_namespace_recorded_at'
+
+ def up
+ add_concurrent_index :analytics_devops_adoption_snapshots, [:namespace_id, :end_time],
+ name: SNAPSHOT_END_TIME_INDEX_NAME
+ add_concurrent_index :analytics_devops_adoption_snapshots, [:namespace_id, :recorded_at],
+ name: SNAPSHOT_RECORDED_AT_INDEX_NAME
+ add_concurrent_index :analytics_devops_adoption_segments, [:display_namespace_id, :namespace_id],
+ unique: true, name: SEGMENTS_INDEX_NAME
+
+ add_concurrent_foreign_key :analytics_devops_adoption_snapshots, :namespaces, column: :namespace_id
+ add_concurrent_foreign_key :analytics_devops_adoption_segments, :namespaces, column: :display_namespace_id
+ end
+
+ def down
+ remove_foreign_key :analytics_devops_adoption_segments, :namespaces, column: :display_namespace_id
+ remove_foreign_key :analytics_devops_adoption_snapshots, :namespaces, column: :namespace_id
+
+ remove_concurrent_index_by_name :analytics_devops_adoption_segments, SEGMENTS_INDEX_NAME
+ remove_concurrent_index_by_name :analytics_devops_adoption_snapshots, SNAPSHOT_RECORDED_AT_INDEX_NAME
+ remove_concurrent_index_by_name :analytics_devops_adoption_snapshots, SNAPSHOT_END_TIME_INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20210430130259_remove_obsolete_segments_field.rb b/db/post_migrate/20210430130259_remove_obsolete_segments_field.rb
new file mode 100644
index 00000000000..ffdd84582cb
--- /dev/null
+++ b/db/post_migrate/20210430130259_remove_obsolete_segments_field.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class RemoveObsoleteSegmentsField < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ remove_column :analytics_devops_adoption_segments, :name
+ end
+ end
+
+ def down
+ add_column :analytics_devops_adoption_segments, :name, :text
+ add_text_limit :analytics_devops_adoption_segments, :name, 255
+ end
+end
diff --git a/db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb b/db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb
new file mode 100644
index 00000000000..d0a72ff2c43
--- /dev/null
+++ b/db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CopyAdoptionSnapshotNamespace < ActiveRecord::Migration[6.0]
+ def up
+ execute <<-SQL
+ UPDATE analytics_devops_adoption_snapshots snapshots
+ SET namespace_id = segments.namespace_id
+ FROM analytics_devops_adoption_segments segments
+ WHERE snapshots.namespace_id IS NULL AND segments.id = snapshots.segment_id
+ SQL
+ end
+
+ def down
+ execute 'UPDATE analytics_devops_adoption_snapshots SET namespace_id = NULL'
+ end
+end
diff --git a/db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb b/db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb
new file mode 100644
index 00000000000..04f454bea37
--- /dev/null
+++ b/db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CopyAdoptionSegmentsNamespace < ActiveRecord::Migration[6.0]
+ def up
+ execute <<-SQL
+ UPDATE analytics_devops_adoption_segments SET display_namespace_id = namespace_id
+ WHERE display_namespace_id IS NULL
+ SQL
+ end
+
+ def down
+ execute 'UPDATE analytics_devops_adoption_segments SET display_namespace_id = NULL'
+ end
+end
diff --git a/db/post_migrate/20210511051718_create_index_on_notes_note.rb b/db/post_migrate/20210511051718_create_index_on_notes_note.rb
new file mode 100644
index 00000000000..8e0ccb3616b
--- /dev/null
+++ b/db/post_migrate/20210511051718_create_index_on_notes_note.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateIndexOnNotesNote < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DUPLICATE_INDEX_NAME = 'index_notes_on_note_gin_trigram'
+ CURRENT_INDEX_NAME = 'index_notes_on_note_trigram'
+
+ disable_ddl_transaction!
+
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/218410#note_565624409
+ # We are having troubles with the index, and some inserts are taking a long time
+ # so in this migration we are recreating the index
+ def up
+ add_concurrent_index :notes, :note, name: DUPLICATE_INDEX_NAME, using: :gin, opclass: :gin_trgm_ops
+ remove_concurrent_index_by_name :notes, CURRENT_INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :notes, :note, name: CURRENT_INDEX_NAME, using: :gin, opclass: :gin_trgm_ops
+ remove_concurrent_index_by_name :notes, DUPLICATE_INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20210430122951 b/db/schema_migrations/20210430122951
new file mode 100644
index 00000000000..43c90a10f22
--- /dev/null
+++ b/db/schema_migrations/20210430122951
@@ -0,0 +1 @@
+476dc70eae87ad3ee30e6be8c1afb4a2aec23a09b96daba2afbd9c4e2edb12b9 \ No newline at end of file
diff --git a/db/schema_migrations/20210430124212 b/db/schema_migrations/20210430124212
new file mode 100644
index 00000000000..dd3e8c1f371
--- /dev/null
+++ b/db/schema_migrations/20210430124212
@@ -0,0 +1 @@
+ebdeb56647f3a7ff5620141833c90b796a9ddfed39234bcf8063ca5b3df6c1f3 \ No newline at end of file
diff --git a/db/schema_migrations/20210430124630 b/db/schema_migrations/20210430124630
new file mode 100644
index 00000000000..2366ab58ef4
--- /dev/null
+++ b/db/schema_migrations/20210430124630
@@ -0,0 +1 @@
+7f6862205e8c315da8433083fc5391f8889951f62d466e0048063322a46f9cc7 \ No newline at end of file
diff --git a/db/schema_migrations/20210430130259 b/db/schema_migrations/20210430130259
new file mode 100644
index 00000000000..b8064b30f52
--- /dev/null
+++ b/db/schema_migrations/20210430130259
@@ -0,0 +1 @@
+c4a4b214f15a1a8d7f6832782d50077189281ca9a9b1b746a0a3bc3af4a47e3c \ No newline at end of file
diff --git a/db/schema_migrations/20210430134202 b/db/schema_migrations/20210430134202
new file mode 100644
index 00000000000..cb9eee98cc0
--- /dev/null
+++ b/db/schema_migrations/20210430134202
@@ -0,0 +1 @@
+77e2b8c1c6054a80122f97dda1e843149fefb7bf6694fdfa897d691d61162d81 \ No newline at end of file
diff --git a/db/schema_migrations/20210430135954 b/db/schema_migrations/20210430135954
new file mode 100644
index 00000000000..9e201905704
--- /dev/null
+++ b/db/schema_migrations/20210430135954
@@ -0,0 +1 @@
+c5fe6f74822168599ad5069bb7c793ec96a4bba99d15ad29cb161ef24291b56d \ No newline at end of file
diff --git a/db/schema_migrations/20210511051718 b/db/schema_migrations/20210511051718
new file mode 100644
index 00000000000..cf6cb7c0ee9
--- /dev/null
+++ b/db/schema_migrations/20210511051718
@@ -0,0 +1 @@
+2d11da499f49964f37cc0a2c541cf58182416ae7b6b0e762a135b327099de1a4 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6ebfbf4409a..fac2cac7b06 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9103,12 +9103,11 @@ ALTER SEQUENCE analytics_devops_adoption_segment_selections_id_seq OWNED BY anal
CREATE TABLE analytics_devops_adoption_segments (
id bigint NOT NULL,
- name text,
last_recorded_at timestamp with time zone,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
namespace_id integer,
- CONSTRAINT check_4be7a006fd CHECK ((char_length(name) <= 255))
+ display_namespace_id integer
);
CREATE SEQUENCE analytics_devops_adoption_segments_id_seq
@@ -9133,7 +9132,8 @@ CREATE TABLE analytics_devops_adoption_snapshots (
security_scan_succeeded boolean NOT NULL,
end_time timestamp with time zone NOT NULL,
total_projects_count integer,
- code_owners_used_count integer
+ code_owners_used_count integer,
+ namespace_id integer
);
CREATE SEQUENCE analytics_devops_adoption_snapshots_id_seq
@@ -22133,6 +22133,12 @@ CREATE INDEX idx_container_repositories_on_exp_cleanup_status_and_start_date ON
CREATE INDEX idx_deployment_clusters_on_cluster_id_and_kubernetes_namespace ON deployment_clusters USING btree (cluster_id, kubernetes_namespace);
+CREATE INDEX idx_devops_adoption_segments_namespace_end_time ON analytics_devops_adoption_snapshots USING btree (namespace_id, end_time);
+
+CREATE INDEX idx_devops_adoption_segments_namespace_recorded_at ON analytics_devops_adoption_snapshots USING btree (namespace_id, recorded_at);
+
+CREATE UNIQUE INDEX idx_devops_adoption_segments_namespaces_pair ON analytics_devops_adoption_segments USING btree (display_namespace_id, namespace_id);
+
CREATE INDEX idx_eaprpb_external_approval_rule_id ON external_approval_rules_protected_branches USING btree (external_approval_rule_id);
CREATE INDEX idx_elastic_reindexing_slices_on_elastic_reindexing_subtask_id ON elastic_reindexing_slices USING btree (elastic_reindexing_subtask_id);
@@ -23667,7 +23673,7 @@ CREATE INDEX index_notes_on_discussion_id ON notes USING btree (discussion_id);
CREATE INDEX index_notes_on_line_code ON notes USING btree (line_code);
-CREATE INDEX index_notes_on_note_trigram ON notes USING gin (note gin_trgm_ops);
+CREATE INDEX index_notes_on_note_gin_trigram ON notes USING gin (note gin_trgm_ops);
CREATE INDEX index_notes_on_noteable_id_and_noteable_type_and_system ON notes USING btree (noteable_id, noteable_type, system);
@@ -25224,6 +25230,9 @@ ALTER TABLE ONLY project_features
ALTER TABLE ONLY ci_pipelines
ADD CONSTRAINT fk_190998ef09 FOREIGN KEY (external_pull_request_id) REFERENCES external_pull_requests(id) ON DELETE SET NULL;
+ALTER TABLE ONLY analytics_devops_adoption_segments
+ ADD CONSTRAINT fk_190a24754d FOREIGN KEY (display_namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY user_details
ADD CONSTRAINT fk_190e4fcc88 FOREIGN KEY (provisioned_by_group_id) REFERENCES namespaces(id) ON DELETE SET NULL;
@@ -25440,6 +25449,9 @@ ALTER TABLE ONLY users
ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_78a6492f68 FOREIGN KEY (repository_updated_event_id) REFERENCES geo_repository_updated_events(id) ON DELETE CASCADE;
+ALTER TABLE ONLY analytics_devops_adoption_snapshots
+ ADD CONSTRAINT fk_78c9eac821 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY lists
ADD CONSTRAINT fk_7a5553d60f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE;
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index cba4deb3f55..f29db9ead38 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -129,6 +129,7 @@ The following metrics are available:
| `pipeline_graph_links_per_job_ratio` | Histogram | 13.9 | Ratio of links to job per graph | |
| `gitlab_ci_pipeline_security_orchestration_policy_processing_duration_seconds` | Histogram | 13.12 | Time in seconds it takes to process Security Policies in CI/CD pipeline | |
| `gitlab_ci_difference_live_vs_actual_minutes` | Histogram | 13.12 | Difference between CI minute consumption counted while jobs were running (live) vs when jobs are complete (actual). Used to enforce CI minute consumption limits on long running jobs. | `plan` |
+| `gitlab_spamcheck_request_duration_seconds` | Histogram | 13.12 | The duration for requests between Rails and the anti-spam engine | |
## Metrics controlled by a feature flag
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 404e28c58c6..bf2aba9f88c 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -864,13 +864,6 @@ configuration is tried to be resolved automatically before reporting an error.
### Object storage settings
-WARNING:
-With the following settings, Pages uses both NFS and Object Storage locations when deploying the
-site. **Do not remove the existing NFS mount used by Pages** when applying these settings. For more
-information, see the epics
-[3901](https://gitlab.com/groups/gitlab-org/-/epics/3901#how-to-test-object-storage-integration-in-beta)
-and [3910](https://gitlab.com/groups/gitlab-org/-/epics/3910).
-
The following settings are:
- Nested under `pages:` and then `object_store:` on source installations.
@@ -882,6 +875,10 @@ The following settings are:
| `remote_directory` | The name of the bucket where Pages site content is stored. | |
| `connection` | Various connection options described below. | |
+NOTE:
+If you want to stop using and disconnect the NFS server, you need to [explicitly disable
+local storage](#disable-pages-local-storage), and it's only possible after upgrading to GitLab 13.11.
+
#### S3-compatible connection settings
See [the available connection settings for different providers](../object_storage.md#connection-settings).
@@ -915,6 +912,8 @@ In Omnibus installations:
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
for the changes to take effect.
+1. [Migrate existing Pages deployments to object storage.](#migrate-pages-deployments-to-object-storage)
+
In installations from source:
1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
@@ -934,6 +933,8 @@ In installations from source:
1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source)
for the changes to take effect.
+1. [Migrate existing Pages deployments to object storage.](#migrate-pages-deployments-to-object-storage)
+
## ZIP storage
In GitLab 14.0 the underlaying storage format of GitLab Pages is changing from
@@ -1019,6 +1020,43 @@ After the migration to object storage is performed, you can choose to revert you
sudo gitlab-rake gitlab:pages:deployments:migrate_to_local
```
+### Disable Pages local storage
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/301159) in GitLab 13.11.
+
+If you use [object storage](#using-object-storage), disable local storage:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['pages_local_store_enabled'] = false
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+Starting from GitLab 13.12, this setting also disables the [legacy storage](#migrate-legacy-storage-to-zip-storage), so if you were using NFS to serve Pages, you can completely disconnect from it.
+
+## Migrate GitLab Pages to 14.0
+
+In GitLab 14.0 a number of breaking changes are introduced which may require some user intervention.
+The steps below describe the best way to migrate without causing any downtime for your GitLab instance.
+
+If you run GitLab on a single server, then most likely you will not notice any problem after
+upgrading to GitLab 14.0, but it may be safer to follow the steps anyway.
+If you run GitLab on a single server, then most likely the upgrade process to 14.0 will go smoothly for you. Regardless, we recommend everyone follow the migration steps to ensure a successful upgrade.
+If at any point you run into issues, consult the [troubleshooting section](#troubleshooting).
+
+To migrate GitLab Pages to GitLab 14.0:
+
+1. If your current GitLab version is lower than 13.12, then you first need to upgrade to 13.12.
+Upgrading directly to 14.0 may cause downtime for some web-sites hosted on GitLab Pages
+until you finish the following steps.
+1. Enable the [API-based configuration](#gitlab-api-based-configuration), which
+is the default starting from GitLab 14.0. Skip this step if you're already running GitLab 14.0 or above.
+1. If you want to store your pages content in the [object storage](#using-object-storage), make sure to configure it.
+If you want to store the pages content locally or continue using an NFS server, skip this step.
+1. [Migrate legacy storage to ZIP storage.](#migrate-legacy-storage-to-zip-storage)
+
## Backup
GitLab Pages are part of the [regular backup](../../raketasks/backup_restore.md), so there is no separate backup to configure.
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index 8198130c61e..e570b9f31cf 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9188) in GitLab 11.8.
-Manages parent-child [epic relationships](../user/group/epics/index.md#multi-level-child-epics).
+Manages parent-child [epic relationships](../user/group/epics/manage_epics.md#multi-level-child-epics).
Every API call to `epic_links` must be authenticated.
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 51724d32f3a..c3125f52cf2 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -895,6 +895,27 @@ When you want to ensure that no event got called, you can use `expect_no_snowplo
end
```
+#### Test Snowplow context against the schema
+
+The [Snowplow schema matcher](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60480)
+helps to reduce validation errors by testing Snowplow context against the JSON schema.
+The schema matcher accepts the following parameters:
+
+- `schema path`
+- `context`
+
+To add a schema matcher spec:
+
+1. Add a new schema to the [Iglu repository](https://gitlab.com/gitlab-org/iglu),
+ then copy the same schema to the `spec/fixtures/product_intelligence/` directory.
+1. In the copied schema, remove the `"$schema"` key and value. We do not need it for specs
+ and the spec fails if we keep the key, as it tries to look for the schema in the URL.
+1. Use the following snippet to call the schema matcher:
+
+ ```ruby
+ match_snowplow_context_schema(schema_path: '<filename from step 1>', context: <Context Hash> )
+ ```
+
### Table-based / Parameterized tests
This style of testing is used to exercise one piece of code with a comprehensive
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 806a3435a9f..1ce2695901a 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -2134,6 +2134,294 @@ Status: `data_available`
Tiers: `free`, `premium`, `ultimate`
+### `counts.in_product_marketing_email_create_0_cta_clicked`
+
+Total clicks on the create track's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_create_0_sent`
+
+Total sent emails of the create track's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_create_1_cta_clicked`
+
+Total clicks on the create track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_create_1_sent`
+
+Total sent emails of the create track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_create_2_cta_clicked`
+
+Total clicks on the create track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_create_2_sent`
+
+Total sent emails of the create track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_team_0_cta_clicked`
+
+Total clicks on the team track's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_team_0_sent`
+
+Total sent emails of the team track's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_team_1_cta_clicked`
+
+Total clicks on the team track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_team_1_sent`
+
+Total sent emails of the team track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_team_2_cta_clicked`
+
+Total clicks on the team track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_team_2_sent`
+
+Total sent emails of the team track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_trial_0_cta_clicked`
+
+Total clicks on the verify trial's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_trial_0_sent`
+
+Total sent emails of the trial track's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_trial_1_cta_clicked`
+
+Total clicks on the trial track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_trial_1_sent`
+
+Total sent emails of the trial track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_trial_2_cta_clicked`
+
+Total clicks on the trial track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_trial_2_sent`
+
+Total sent emails of the trial track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_verify_0_cta_clicked`
+
+Total clicks on the verify track's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_verify_0_sent`
+
+Total sent emails of the verify track's first email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_verify_1_cta_clicked`
+
+Total clicks on the verify track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_verify_1_sent`
+
+Total sent emails of the verify track's second email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_verify_2_cta_clicked`
+
+Total clicks on the verify track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
+### `counts.in_product_marketing_email_verify_2_sent`
+
+Total sent emails of the verify track's third email
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml)
+
+Group: `group::activation`
+
+Status: `implemented`
+
+Tiers: `free`, `premium`, `ultimate`
+
### `counts.in_review_folder`
Missing description
diff --git a/doc/user/group/epics/img/epic_view_v13.0.png b/doc/user/group/epics/img/epic_view_v13.0.png
deleted file mode 100644
index c317a5707bc..00000000000
--- a/doc/user/group/epics/img/epic_view_v13.0.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/new_epic_from_groups_v13.7.png b/doc/user/group/epics/img/new_epic_from_groups_v13.7.png
deleted file mode 100644
index 3607d5c7a3f..00000000000
--- a/doc/user/group/epics/img/new_epic_from_groups_v13.7.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 206cd5246f1..0608ff38db1 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -8,57 +8,27 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Epics **(PREMIUM)**
> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2.
-> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
+> - Single-level epics [were moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
-Epics let you manage your portfolio of projects more efficiently by tracking groups of [issues](../../project/issues/index.md)
-that share a theme across projects and milestones.
+When [issues](../../project/issues/index.md) share a theme across projects and milestones,
+you can manage them by using epics.
-An epic's page contains the following tabs:
+You can also create child epics, and assign start and end dates,
+which creates a visual roadmap for you to view progress.
-- **Issues**: issues added to this epic.
-- **Epics and Issues**: epics and issues added to this epic.
- Appears instead of the **Issues** tab if you have access to [multi-level epics](#multi-level-child-epics).
- Child epics and their issues are shown in a tree view.
+Use epics:
- - To reveal the child epics and issues, select the chevron (**>**) next to a parent epic.
- - To see a breakdown of open and closed items, hover over the total counts.
-
- The number provided here includes all epics associated with this project. The number includes
- epics for which users may not yet have permission.
-
-- [**Roadmap**](#roadmap-in-epics): a roadmap view of child epics which have start and due dates.
- Appears if you have access to [multi-level epics](#multi-level-child-epics).
-
-![epic view](img/epic_view_v13.0.png)
-
-## Use cases
-
-- Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature.
-- Track when the work for the group of issues is targeted to begin, and when it's targeted to end.
-- Discuss and collaborate on feature ideas and scope at a high level.
-
-## Manage epics
-
-To learn what you can do with an epic, see [Manage epics](manage_epics.md). Possible actions include:
-
-- [Create an epic](manage_epics.md#create-an-epic)
-- [Edit an epic](manage_epics.md#edit-an-epic)
-- [Bulk-edit epics](manage_epics.md#bulk-edit-epics)
-- [Delete an epic](manage_epics.md#delete-an-epic)
-- [Close an epic](manage_epics.md#close-an-epic)
-- [Reopen a closed epic](manage_epics.md#reopen-a-closed-epic)
-- [Go to an epic from an issue](manage_epics.md#go-to-an-epic-from-an-issue)
-- [Search for an epic from epics list page](manage_epics.md#search-for-an-epic-from-epics-list-page)
-- [Make an epic confidential](manage_epics.md#make-an-epic-confidential)
-- [Manage issues assigned to an epic](manage_epics.md#manage-issues-assigned-to-an-epic)
-- [Manage multi-level child epics **(ULTIMATE)**](manage_epics.md#manage-multi-level-child-epics)
+- When your team is working on a large feature that involves multiple discussions
+ in different issues in different projects in a [group](../index.md).
+- To track when the work for the group of issues is targeted to begin and end.
+- To discuss and collaborate on feature ideas and scope at a high level.
## Relationships between epics and issues
The possible relationships between epics and issues are:
- An epic is the parent of one or more issues.
-- An epic is the parent of one or more child epics. For details see [Multi-level child epics](#multi-level-child-epics). **(ULTIMATE)**
+- An epic is the parent of one or more child epics. For details see [Multi-level child epics](manage_epics.md#multi-level-child-epics).
```mermaid
graph TD
@@ -67,72 +37,13 @@ graph TD
Child_epic --> Issue2
```
-See [Manage issues assigned to an epic](manage_epics.md#manage-issues-assigned-to-an-epic) for steps
-to:
-
-- Add an issue to an epic
-- Reorder issues
-- Move an issue between epics
-- Promote an issue to an epic
-
-## Issue health status in Epic tree **(ULTIMATE)**
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/199184) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
-> - The health status of a closed issue [is hidden](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3 or later.
-> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/213567) in GitLab 13.7.
-
-Report or respond to the health of issues and epics by setting a red, amber, or green [health status](../../project/issues/managing_issues.md#health-status), which then appears on your Epic tree.
-
-## Multi-level child epics **(ULTIMATE)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8333) in GitLab Ultimate 11.7.
-
-You can add any epic that belongs to a group or subgroup of the parent epic's group.
-New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
-
-When you add an epic that's already linked to a parent epic, the link to its current parent is removed.
-
-Epics can contain multiple nested child epics, up to a total of seven levels deep.
-
-See [Manage multi-level child epics](manage_epics.md#manage-multi-level-child-epics) for
-steps to create, move, reorder, or delete child epics.
-
-## Start date and due date
-
-To set a **Start date** and **Due date** for an epic, select one of the following:
-
-- **Fixed**: enter a fixed value.
-- **Inherited**: inherit a dynamic value from the epic's issues, child epics, and milestones.
-
-### Inherited
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7332) in GitLab 12.5 to replace **From milestones**.
-
-If you select **Inherited**:
-
-- For the **start date**: GitLab scans all child epics and issues assigned to the epic,
- and sets the start date to match the earliest found start date or milestone.
-- For the **due date**: GitLab sets the due date to match the latest due date or
- milestone found among its child epics and issues.
-
-These are dynamic dates and recalculated if any of the following occur:
-
-- A child epic's dates change.
-- Milestones are reassigned to an issue.
-- A milestone's dates change.
-- Issues are added to, or removed from, the epic.
-
-Because the epic's dates can inherit dates from its children, the start date and due date propagate from the bottom to the top.
-If the start date of a child epic on the lowest level changes, that becomes the earliest possible start date for its parent epic.
-The parent epic's start date then reflects this change and propagates upwards to the top epic.
-
## Roadmap in epics **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10.
-If your epic contains one or more [child epics](#multi-level-child-epics) which
-have a [start or due date](#start-date-and-due-date), a
-[roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic.
+If your epic contains one or more [child epics](manage_epics.md#multi-level-child-epics) that
+have a start or due date, a visual
+[roadmap](../roadmap/index.md) of the child epics is listed under the parent epic.
![Child epics roadmap](img/epic_view_roadmap_v12_9.png)
@@ -144,45 +55,19 @@ epic's issue list.
If you have access to edit an epic and an issue added to that epic, you can add the issue to or
remove it from the epic.
-Note that for a given group, the visibility of all projects must be the same as
+For a given group, the visibility of all projects must be the same as
the group, or less restrictive. That means if you have access to a group's epic,
then you already have access to its projects' issues.
You can also consult the [group permissions table](../../permissions.md#group-members-permissions).
-## Thread
-
-- Comments: collaborate on that epic by posting comments in its thread.
- These text fields also fully support
- [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown).
-
-## Comment or start a thread
-
-Once you write your comment, you can either:
-
-- Click **Comment** to publish your comment.
-- Click **Start thread** to start a thread within that epic's discussion.
-
-### Activity sort order
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214364) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
-
-You can reverse the default order and interact with the activity feed sorted by most recent items
-at the top. Your preference is saved via local storage and automatically applied to every issue
-you view.
-
-To change the activity sort order, click the **Oldest first** dropdown menu and select either oldest
-or newest items to be shown first.
-
-![Issue activity sort order dropdown button](img/epic_activity_sort_order_v13_2.png)
-
-## Award emoji
-
-You can [award an emoji](../../award_emojis.md) to that epic or its comments.
-
-## Notifications
+## Related topics
-You can [turn on notifications](../../profile/notifications.md) to be alerted about epic events.
+- [Manage epics](manage_epics.md) and multi-level child epics.
+- [Turn on notifications](../../profile/notifications.md) for about epic events.
+- [Award an emoji](../../award_emojis.md) to an epic or its comments.
+- Collaborate on an epic by posting comments in a [thread](../../discussions/index.md).
+- Use [health status](../../project/issues/managing_issues.md#health-status) to track your progress.
<!-- ## Troubleshooting
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 03ff9b9b165..4a6fa1fb9fe 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -16,28 +16,46 @@ to them.
> - The New Epic form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211533) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
> - In [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/229621) and later, the New Epic button on the Epics list opens the New Epic form.
-> - In [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45948) and later, you can create a new epic from an empty Roadmap.
+> - In [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45948) and later, you can create a new epic from an empty roadmap.
To create an epic in the group you're in:
1. Get to the New Epic form:
- - From the **Epics** list in your group, select **New epic**.
+ - Go to your group and from the left sidebar select **Epics**. Then select **New epic**.
- From an epic in your group, select **New epic**.
- From anywhere, in the top menu, select **New...** (**{plus-square}**) **> New epic**.
- In an empty [roadmap](../roadmap/index.md), select **New epic**.
- ![New epic from an open epic](img/new_epic_from_groups_v13.7.png)
+1. Enter a title.
+1. Optional. Enter a description.
+1. Optional. To make the epic confidential, select the [Confidentiality checkbox](#make-an-epic-confidential).
+1. Optional. Choose labels.
+1. Optional. Select a start and due date, or [inherit](#start-and-due-date-inheritance) them.
+1. Select **Create epic**.
-1. Fill in these fields:
+The newly created epic opens.
- - Title
- - Description
- - [Confidentiality checkbox](#make-an-epic-confidential)
- - Labels
- - Start date
- - Due date
+### Start and due date inheritance
-1. Select **Create epic**. You are taken to view the newly created epic.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7332) in GitLab 12.5 to replace **From milestones**.
+
+If you select **Inherited**:
+
+- For the **start date**: GitLab scans all child epics and issues assigned to the epic,
+ and sets the start date to match the earliest found start date or milestone.
+- For the **due date**: GitLab sets the due date to match the latest due date or
+ milestone found among its child epics and issues.
+
+These are dynamic dates and recalculated if any of the following occur:
+
+- A child epic's dates change.
+- Milestones are reassigned to an issue.
+- A milestone's dates change.
+- Issues are added to, or removed from, the epic.
+
+Because the epic's dates can inherit dates from its children, the start date and due date propagate from the bottom to the top.
+If the start date of a child epic on the lowest level changes, that becomes the earliest possible start date for its parent epic.
+The parent epic's start date then reflects this change and propagates upwards to the top epic.
## Edit an epic
@@ -55,7 +73,7 @@ To edit an epic's title or description:
1. Make your changes.
1. Select **Save changes**.
-To edit an epics' start date, due date, or labels:
+To edit an epic's start date, due date, or labels:
1. Select **Edit** next to each section in the epic sidebar.
1. Select the dates or labels for your epic.
@@ -151,6 +169,19 @@ The sort option and order is saved and used wherever you browse epics, including
![epics sort](img/epics_sort.png)
+## Change activity sort order
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214364) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
+
+You can reverse the default order and interact with the activity feed sorted by most recent items
+at the top. Your preference is saved via local storage and automatically applied to every epic and issue
+you view.
+
+To change the activity sort order, click the **Oldest first** dropdown menu and select either oldest
+or newest items to be shown first.
+
+![Issue activity sort order dropdown button](img/epic_activity_sort_order_v13_2.png)
+
## Make an epic confidential
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213068) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0 behind a feature flag, disabled by default.
@@ -200,6 +231,13 @@ To add a new issue to an epic:
If there are multiple issues to be added, press <kbd>Space</kbd> and repeat this step.
1. Select **Add**.
+#### View count of issues in an epic
+
+On the **Epics and Issues** tab, under each epic name, hover over the total counts.
+
+The number indicates all epics associated with the project, including issues
+you might not have permission to.
+
#### Create an issue from an epic
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5419) in GitLab 12.7.
@@ -286,9 +324,16 @@ For an introduction to epic templates, see [GitLab Epics and Epic Template Tip](
For more on epic templates, see [Epic Templates - Repeatable sets of issues](https://about.gitlab.com/handbook/marketing/strategic-marketing/getting-started/104/).
-## Manage multi-level child epics **(ULTIMATE)**
+## Multi-level child epics **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8333) in GitLab Ultimate 11.7.
+
+You can add any epic that belongs to a group or subgroup of the parent epic's group.
+New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
+
+When you add an epic that's already linked to a parent epic, the link to its current parent is removed.
-With [multi-level epics](index.md#multi-level-child-epics), you can manage more complex projects.
+Epics can contain multiple nested child epics, up to a total of seven levels deep.
### Add a child epic to an epic
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 81166e2dfbd..9e8a75743a7 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -62,7 +62,7 @@ When you're creating a new issue, these are the fields you can fill in:
- Checkbox to make the issue confidential
- Assignee
- Weight
-- Epic **(PREMIUM)**
+- [Epic](../../group/epics/index.md)
- Due date
- Milestone
- Labels
@@ -369,7 +369,7 @@ in a comment or description field.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17589) in GitLab 13.3.
Assignees in the sidebar are updated in real time. This feature is **disabled by default**.
-To enable it, you need to enable [ActionCable in-app mode](https://docs.gitlab.com/omnibus/settings/actioncable.html).
+To enable it, you need to enable [Action Cable in-app mode](https://docs.gitlab.com/omnibus/settings/actioncable.html).
## Similar issues
@@ -402,5 +402,4 @@ This marks issues as progressing as planned or needs attention to keep on schedu
After an issue is closed, its health status can't be edited and the **Edit** button becomes disabled
until the issue is reopened.
-You can then see issue statuses in the issues list and the
-[epic tree](../../group/epics/index.md#issue-health-status-in-epic-tree).
+You can then see issue statuses in the issues list and the epic tree.
diff --git a/lib/banzai/filter/references/snippet_reference_filter.rb b/lib/banzai/filter/references/snippet_reference_filter.rb
index a2971e28de6..502bfca1ab7 100644
--- a/lib/banzai/filter/references/snippet_reference_filter.rb
+++ b/lib/banzai/filter/references/snippet_reference_filter.rb
@@ -11,10 +11,14 @@ module Banzai
self.reference_type = :snippet
self.object_class = Snippet
- def find_object(project, id)
+ def parent_records(project, ids)
return unless project.is_a?(Project)
- project.snippets.find_by(id: id)
+ project.snippets.where(id: ids.to_a)
+ end
+
+ def find_object(project, id)
+ reference_cache.records_per_parent[project][id]
end
def url_for_object(snippet, project)
diff --git a/lib/gitlab/database/reindexing/concurrent_reindex.rb b/lib/gitlab/database/reindexing/concurrent_reindex.rb
index d08773c1db7..7e2dd55d21b 100644
--- a/lib/gitlab/database/reindexing/concurrent_reindex.rb
+++ b/lib/gitlab/database/reindexing/concurrent_reindex.rb
@@ -11,7 +11,14 @@ module Gitlab
PG_IDENTIFIER_LENGTH = 63
TEMPORARY_INDEX_PREFIX = 'tmp_reindex_'
REPLACED_INDEX_PREFIX = 'old_reindex_'
- STATEMENT_TIMEOUT = 6.hours
+ STATEMENT_TIMEOUT = 9.hours
+
+ # When dropping an index, we acquire a SHARE UPDATE EXCLUSIVE lock,
+ # which only conflicts with DDL and vacuum. We therefore execute this with a rather
+ # high lock timeout and a long pause in between retries. This is an alternative to
+ # setting a high statement timeout, which would lead to a long running query with effects
+ # on e.g. vacuum.
+ REMOVE_INDEX_RETRY_CONFIG = [[1.minute, 9.minutes]] * 30
attr_reader :index, :logger
@@ -95,7 +102,13 @@ module Gitlab
def remove_index(schema, name)
logger.info("Removing index #{schema}.#{name}")
- set_statement_timeout do
+ retries = Gitlab::Database::WithLockRetriesOutsideTransaction.new(
+ timing_configuration: REMOVE_INDEX_RETRY_CONFIG,
+ klass: self.class,
+ logger: logger
+ )
+
+ retries.run(raise_on_exhaustion: false) do
connection.execute(<<~SQL)
DROP INDEX CONCURRENTLY
IF EXISTS #{quote_table_name(schema)}.#{quote_table_name(name)}
@@ -121,7 +134,6 @@ module Gitlab
def with_lock_retries(&block)
arguments = { klass: self.class, logger: logger }
-
Gitlab::Database::WithLockRetries.new(**arguments).run(raise_on_exhaustion: true, &block)
end
diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb
index 3fb52d786ad..bbf8f133f0f 100644
--- a/lib/gitlab/database/with_lock_retries.rb
+++ b/lib/gitlab/database/with_lock_retries.rb
@@ -92,7 +92,7 @@ module Gitlab
end
begin
- run_block_with_transaction
+ run_block_with_lock_timeout
rescue ActiveRecord::LockWaitTimeout
if retry_with_lock_timeout?
disable_idle_in_transaction_timeout if ActiveRecord::Base.connection.transaction_open?
@@ -121,7 +121,7 @@ module Gitlab
block.call
end
- def run_block_with_transaction
+ def run_block_with_lock_timeout
ActiveRecord::Base.transaction(requires_new: true) do
execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'")
diff --git a/lib/gitlab/database/with_lock_retries_outside_transaction.rb b/lib/gitlab/database/with_lock_retries_outside_transaction.rb
new file mode 100644
index 00000000000..175cc493e36
--- /dev/null
+++ b/lib/gitlab/database/with_lock_retries_outside_transaction.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ # This retry method behaves similar to WithLockRetries
+ # except it does not wrap itself into a transaction scope.
+ #
+ # In our context, this is only useful if directly connected to
+ # PostgreSQL. When going through pgbouncer, this method **won't work**
+ # as it relies on using `SET` outside transactions (and hence can be
+ # multiplexed across different connections).
+ class WithLockRetriesOutsideTransaction < WithLockRetries
+ private
+
+ def run_block_with_lock_timeout
+ execute("SET lock_timeout TO '#{current_lock_timeout_in_ms}ms'")
+
+ log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
+
+ run_block
+
+ log(message: 'Migration finished', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
+ end
+
+ def run_block_without_lock_timeout
+ log(message: "Couldn't acquire lock to perform the migration", current_iteration: current_iteration)
+ log(message: "Executing without lock timeout", current_iteration: current_iteration)
+
+ disable_lock_timeout
+
+ run_block
+
+ log(message: 'Migration finished', current_iteration: current_iteration)
+ end
+
+ def disable_lock_timeout
+ execute("SET lock_timeout TO '0'")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pages/settings.rb b/lib/gitlab/pages/settings.rb
index 8c97d25c5e7..b35683c9dec 100644
--- a/lib/gitlab/pages/settings.rb
+++ b/lib/gitlab/pages/settings.rb
@@ -11,10 +11,6 @@ module Gitlab
super
end
- def local_store
- @local_store ||= ::Gitlab::Pages::Stores::LocalStore.new(super)
- end
-
private
def disk_access_denied?
diff --git a/lib/gitlab/pages/stores/local_store.rb b/lib/gitlab/pages/stores/local_store.rb
deleted file mode 100644
index 68a7ebaceff..00000000000
--- a/lib/gitlab/pages/stores/local_store.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Pages
- module Stores
- class LocalStore < ::SimpleDelegator
- def enabled
- return false unless Feature.enabled?(:pages_update_legacy_storage, default_enabled: true)
-
- super
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index cc7fa572f32..40e386e717b 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -190,7 +190,8 @@ module Gitlab
user_preferences_usage,
ingress_modsecurity_usage,
container_expiration_policies_usage,
- service_desk_counts
+ service_desk_counts,
+ email_campaign_counts
).tap do |data|
data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
end
@@ -845,6 +846,28 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
+ def email_campaign_counts
+ # rubocop:disable UsageData/LargeTable
+ sent_emails = count(Users::InProductMarketingEmail.group(:track, :series))
+ clicked_emails = count(Users::InProductMarketingEmail.where.not(cta_clicked_at: nil).group(:track, :series))
+
+ series_amount = Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.count
+
+ Users::InProductMarketingEmail.tracks.keys.each_with_object({}) do |track, result|
+ # rubocop: enable UsageData/LargeTable:
+ 0.upto(series_amount - 1).map do |series|
+ # When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
+ sent_count = sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails
+ clicked_count = clicked_emails.is_a?(Hash) ? clicked_emails.fetch([track, series], 0) : clicked_emails
+
+ result["in_product_marketing_email_#{track}_#{series}_sent"] = sent_count
+ result["in_product_marketing_email_#{track}_#{series}_cta_clicked"] = clicked_count
+ end
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def unique_visit_service
strong_memoize(:unique_visit_service) do
::Gitlab::Analytics::UniqueVisits.new
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5f34d8d8aee..8d886689f42 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -978,6 +978,9 @@ msgstr ""
msgid "%{user_name} profile page"
msgstr ""
+msgid "%{username} changed the draft status of merge request %{mr_link}"
+msgstr ""
+
msgid "%{username} has asked for a GitLab account on your instance %{host}:"
msgstr ""
@@ -11797,7 +11800,7 @@ msgstr ""
msgid "Draft merge requests can't be merged."
msgstr ""
-msgid "Drop or %{linkStart}upload%{linkEnd} designs to attach"
+msgid "Drag your designs here or %{linkStart}click to upload%{linkEnd}."
msgstr ""
msgid "Drop or %{linkStart}upload%{linkEnd} file to attach"
diff --git a/qa/qa.rb b/qa/qa.rb
index 446cf5d0b87..dc730e555dd 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -65,6 +65,7 @@ module QA
autoload :ApiFabricator, 'qa/resource/api_fabricator'
autoload :Base, 'qa/resource/base'
+ autoload :GroupBase, 'qa/resource/group_base'
autoload :Sandbox, 'qa/resource/sandbox'
autoload :Group, 'qa/resource/group'
autoload :Issue, 'qa/resource/issue'
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
index fb7236f9f4c..c7565871b0b 100644
--- a/qa/qa/resource/group.rb
+++ b/qa/qa/resource/group.rb
@@ -2,10 +2,8 @@
module QA
module Resource
- class Group < Base
- include Members
-
- attr_accessor :path, :description
+ class Group < GroupBase
+ attr_accessor :description
attribute :sandbox do
Sandbox.fabricate_via_api! do |sandbox|
@@ -13,10 +11,6 @@ module QA
end
end
- attribute :full_path
- attribute :id
- attribute :name
- attribute :runners_token
attribute :require_two_factor_authentication
def initialize
@@ -59,14 +53,6 @@ module QA
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end
- def api_put_path
- "/groups/#{id}"
- end
-
- def api_post_path
- '/groups'
- end
-
def api_post_body
{
parent_id: sandbox.id,
@@ -77,17 +63,14 @@ module QA
}
end
- def api_delete_path
- "/groups/#{id}"
- end
-
def set_require_two_factor_authentication(value:)
put_body = { require_two_factor_authentication: value }
response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
+ return if response.code == HTTP_STATUS_OK
- unless response.code == HTTP_STATUS_OK
- raise ResourceUpdateFailedError, "Could not update require_two_factor_authentication to #{value}. Request returned (#{response.code}): `#{response}`."
- end
+ raise(ResourceUpdateFailedError, <<~ERROR.strip)
+ Could not update require_two_factor_authentication to #{value}. Request returned (#{response.code}): `#{response}`.
+ ERROR
end
def change_repository_storage(new_storage)
@@ -95,12 +78,20 @@ module QA
response = post Runtime::API::Request.new(api_client, "/groups/#{id}/repository_storage_moves").url, post_body
unless response.code.between?(200, 300)
- raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
+ raise(
+ ResourceUpdateFailedError,
+ "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
+ )
end
- wait_until(sleep_interval: 1) { Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished', new_storage) }
+ wait_until(sleep_interval: 1) do
+ Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished', new_storage)
+ end
rescue Support::Repeater::RepeaterConditionExceededError
- raise Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError, 'Timed out while waiting for the group repository storage move to finish'
+ raise(
+ Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError,
+ 'Timed out while waiting for the group repository storage move to finish'
+ )
end
end
end
diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb
new file mode 100644
index 00000000000..bdd442a1c8b
--- /dev/null
+++ b/qa/qa/resource/group_base.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ # Base class for group classes Resource::Sandbox and Resource::Group
+ #
+ class GroupBase < Base
+ include Members
+
+ attr_accessor :path
+
+ attribute :id
+ attribute :runners_token
+ attribute :name
+ attribute :full_path
+
+ # API post path
+ #
+ # @return [String]
+ def api_post_path
+ '/groups'
+ end
+
+ # API put path
+ #
+ # @return [String]
+ def api_put_path
+ "/groups/#{id}"
+ end
+
+ # API delete path
+ #
+ # @return [String]
+ def api_delete_path
+ "/groups/#{id}"
+ end
+
+ # Object comparison
+ #
+ # @param [QA::Resource::GroupBase] other
+ # @return [Boolean]
+ def ==(other)
+ other.is_a?(GroupBase) && comparable_group == other.comparable_group
+ end
+
+ # Override inspect for a better rspec failure diff output
+ #
+ # @return [String]
+ def inspect
+ JSON.pretty_generate(comparable_group)
+ end
+
+ protected
+
+ # Return subset of fields for comparing groups
+ #
+ # @return [Hash]
+ def comparable_group
+ reload! if api_response.nil?
+
+ api_resource.except(
+ :id,
+ :web_url,
+ :visibility,
+ :full_name,
+ :full_path,
+ :created_at,
+ :parent_id,
+ :runners_token
+ )
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb
index ae183d55d89..913fd6ab9ec 100644
--- a/qa/qa/resource/sandbox.rb
+++ b/qa/qa/resource/sandbox.rb
@@ -6,16 +6,7 @@ module QA
# Ensure we're in our sandbox namespace, either by navigating to it or by
# creating it if it doesn't yet exist.
#
- class Sandbox < Base
- include Members
-
- attr_accessor :path
-
- attribute :id
- attribute :runners_token
- attribute :name
- attribute :full_path
-
+ class Sandbox < GroupBase
def initialize
@path = Runtime::Namespace.sandbox_name
end
@@ -56,18 +47,6 @@ module QA
"/groups/#{path}"
end
- def api_members_path
- "#{api_get_path}/members"
- end
-
- def api_post_path
- '/groups'
- end
-
- def api_delete_path
- "/groups/#{id}"
- end
-
def api_post_body
{
path: path,
@@ -76,17 +55,14 @@ module QA
}
end
- def api_put_path
- "/groups/#{id}"
- end
-
def update_group_setting(group_setting:, value:)
- put_body = { "#{group_setting}": value }
- response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
+ response = put(Runtime::API::Request.new(api_client, api_put_path).url, { "#{group_setting}": value })
+ return if response.code == HTTP_STATUS_OK
- unless response.code == HTTP_STATUS_OK
- raise ResourceUpdateFailedError, "Could not update #{group_setting} to #{value}. Request returned (#{response.code}): `#{response}`."
- end
+ raise(
+ ResourceUpdateFailedError,
+ "Could not update #{group_setting} to #{value}. Request returned (#{response.code}): `#{response}`."
+ )
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
index 39e77346a0e..055300122d4 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
@@ -26,30 +26,27 @@ module QA
end
end
+ let!(:subgroup) do
+ Resource::Group.fabricate_via_api! do |group|
+ group.api_client = api_client
+ group.sandbox = source_group
+ group.path = "subgroup-for-import-#{SecureRandom.hex(4)}"
+ end
+ end
+
let(:imported_group) do
Resource::Group.new.tap do |group|
group.api_client = api_client
group.path = source_group.path
- end.reload!
- rescue Resource::ApiFabricator::ResourceNotFoundError
- nil
+ end
end
- # Return subset of fields for comparing groups
- #
- # @param [Resource::Group, nil] group
- # @return [Hash]
- def comparable_group(group)
- group&.api_resource&.except(
- :id,
- :web_url,
- :visibility,
- :full_name,
- :full_path,
- :created_at,
- :parent_id,
- :runners_token
- )
+ let(:imported_subgroup) do
+ Resource::Group.new.tap do |group|
+ group.api_client = api_client
+ group.sandbox = imported_group
+ group.path = subgroup.path
+ end
end
def staging?
@@ -73,15 +70,15 @@ module QA
it(
'performs bulk group import from another gitlab instance',
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785',
- # https://gitlab.com/gitlab-org/gitlab/-/issues/330344
- exclude: { job: ['ce:relative_url', 'ee:relative_url'] }
+ exclude: { job: ['ce:relative_url', 'ee:relative_url'] } # https://gitlab.com/gitlab-org/gitlab/-/issues/330344
) do
Page::Group::BulkImport.perform do |import_page|
import_page.import_group(source_group.path, sandbox.path)
aggregate_failures do
expect(import_page).to have_imported_group(source_group.path, wait: 120)
- expect(comparable_group(imported_group)).to eq(comparable_group(source_group))
+ expect(imported_group).to eq(source_group)
+ expect(imported_subgroup).to eq(subgroup)
end
end
end
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 0efa3519fca..0d8a46bdd2d 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -136,7 +136,7 @@ module Trigger
def extra_variables
# Use CI_MERGE_REQUEST_SOURCE_BRANCH_SHA for omnibus checkouts due to pipeline for merged results
- # and fallback to CI_COMMIT_SHA (merged result commit) for the non-MR pipelines.
+ # and fallback to CI_COMMIT_SHA for the non-MR pipelines.
# See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/index.html#with-pipeline-for-merged-results.
# We also set IMAGE_TAG so the GitLab Docker image is tagged with that SHA.
source_sha = Trigger.non_empty_variable_value('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA') || ENV['CI_COMMIT_SHA']
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index b0d2f90145f..21b39d2da46 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -217,7 +217,7 @@ RSpec.describe 'Group issues page' do
it 'first pagination item is active' do
page.within('.gl-pagination') do
- expect(find('.active')).to have_content('1')
+ expect(find('li.active')).to have_content('1')
end
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 619f7d03f40..88a7b890daa 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -330,7 +330,7 @@ RSpec.describe 'Filter issues', :js do
context 'issue label clicked' do
it 'filters and displays in search bar' do
- find('[data-qa-selector="issuable-label"]', text: multiple_words_label.title).click
+ click_link multiple_words_label.title
expect_issues_list_count(1)
expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
diff --git a/spec/features/issues/service_desk_spec.rb b/spec/features/issues/service_desk_spec.rb
index cc0d35afd60..0a879fdd4d4 100644
--- a/spec/features/issues/service_desk_spec.rb
+++ b/spec/features/issues/service_desk_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe 'Service Desk Issue Tracker', :js do
let_it_be(:support_bot) { User.support_bot }
before do
+ stub_feature_flags(vue_issuables_list: true)
+
# The following two conditions equate to Gitlab::ServiceDesk.supported == true
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb
index d6daf73aba2..29b2f0fffd7 100644
--- a/spec/finders/packages/group_packages_finder_spec.rb
+++ b/spec/finders/packages/group_packages_finder_spec.rb
@@ -122,7 +122,7 @@ RSpec.describe Packages::GroupPackagesFinder do
end
context 'when there are processing packages' do
- let_it_be(:package4) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
+ let_it_be(:package4) { create(:nuget_package, :processing, project: project) }
it { is_expected.to match_array([package1, package2]) }
end
diff --git a/spec/finders/packages/package_finder_spec.rb b/spec/finders/packages/package_finder_spec.rb
index 6a1d857dad4..2bb4f05a41d 100644
--- a/spec/finders/packages/package_finder_spec.rb
+++ b/spec/finders/packages/package_finder_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ::Packages::PackageFinder do
end
context 'processing packages' do
- let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
+ let_it_be(:nuget_package) { create(:nuget_package, :processing, project: project) }
let(:package_id) { nuget_package.id }
it 'are not returned' do
diff --git a/spec/finders/packages/packages_finder_spec.rb b/spec/finders/packages/packages_finder_spec.rb
index 0add77a8478..b72f4aab3ec 100644
--- a/spec/finders/packages/packages_finder_spec.rb
+++ b/spec/finders/packages/packages_finder_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe ::Packages::PackagesFinder do
end
context 'with processing packages' do
- let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
+ let_it_be(:nuget_package) { create(:nuget_package, :processing, project: project) }
it { is_expected.to match_array([conan_package, maven_package]) }
end
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 603baef4a5f..0aa5aa2f691 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -1,5 +1,5 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { getAllByRole } from '@testing-library/dom';
+import { getAllByRole, getByRole } from '@testing-library/dom';
import { shallowMount, createLocalVue, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -25,6 +25,7 @@ describe('Issuable Time Tracking Report', () => {
queryHandler = successIssueQueryHandler,
issuableType = 'issue',
mountFunction = shallowMount,
+ limitToHours = false,
} = {}) => {
fakeApollo = createMockApollo([
[getIssueTimelogsQuery, queryHandler],
@@ -35,6 +36,7 @@ describe('Issuable Time Tracking Report', () => {
issuableId: 1,
issuableType,
},
+ propsData: { limitToHours },
localVue,
apolloProvider: fakeApollo,
});
@@ -94,4 +96,30 @@ describe('Issuable Time Tracking Report', () => {
expect(getAllByRole(wrapper.element, 'row', { name: /Administrator/i })).toHaveLength(3);
});
});
+
+ describe('observes `limit display of time tracking units to hours` setting', () => {
+ describe('when false', () => {
+ beforeEach(() => {
+ mountComponent({ limitToHours: false, mountFunction: mount });
+ });
+
+ it('renders correct results', async () => {
+ await waitForPromises();
+
+ expect(getByRole(wrapper.element, 'columnheader', { name: /1d 30m/i })).not.toBeNull();
+ });
+ });
+
+ describe('when true', () => {
+ beforeEach(() => {
+ mountComponent({ limitToHours: true, mountFunction: mount });
+ });
+
+ it('renders correct results', async () => {
+ await waitForPromises();
+
+ expect(getByRole(wrapper.element, 'columnheader', { name: /8h 30m/i })).not.toBeNull();
+ });
+ });
+ });
});
diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb
index 2e37e34bba5..9e2a6f35910 100644
--- a/spec/lib/banzai/filter/references/reference_cache_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb
@@ -58,6 +58,7 @@ RSpec.describe Banzai::Filter::References::ReferenceCache do
# Since this is an issue filter that is not batching issue queries
# across projects, we have to account for that.
# 1 for both projects, 1 for issues in each project == 3
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359
max_count = control_count + 1
expect do
diff --git a/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
index 32a706925ba..7ab3b24b1c2 100644
--- a/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
@@ -219,4 +219,31 @@ RSpec.describe Banzai::Filter::References::SnippetReferenceFilter do
expect(reference_filter(act, project: nil, group: create(:group)).to_html).to eq exp
end
end
+
+ context 'checking N+1' do
+ let(:namespace2) { create(:namespace) }
+ let(:project2) { create(:project, :public, namespace: namespace2) }
+ let(:snippet2) { create(:project_snippet, project: project2) }
+ let(:reference2) { "#{project2.full_path}$#{snippet2.id}" }
+
+ it 'does not have N+1 per multiple references per project', :use_sql_query_cache do
+ markdown = "#{reference} $9999990"
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ reference_filter(markdown)
+ end.count
+
+ markdown = "#{reference} $9999990 $9999991 $9999992 $9999993 #{reference2} something/cool$12"
+
+ # Since we're not batching snippet queries across projects,
+ # we have to account for that.
+ # 1 for both projects, 1 for snippets in each project == 3
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359
+ max_count = control_count + 1
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(max_count)
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb
index 51fc7c6620b..d9077969003 100644
--- a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb
+++ b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
end
it 'replaces the existing index with an identical index' do
- expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').twice
+ expect(connection).to receive(:execute).with('SET statement_timeout TO \'32400s\'')
expect_to_execute_concurrently_in_order(create_index)
@@ -123,6 +123,10 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
expect_index_rename(replacement_name, index.name)
expect_index_rename(replaced_name, replacement_name)
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: false).and_yield
+ end
+
expect_to_execute_concurrently_in_order(drop_index)
subject.perform
@@ -136,7 +140,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
end
it 'rebuilds table statistics before dropping the original index' do
- expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').twice
+ expect(connection).to receive(:execute).with('SET statement_timeout TO \'32400s\'')
expect_to_execute_concurrently_in_order(create_index)
@@ -152,7 +156,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
expect_index_rename(replacement_name, index.name)
expect_index_rename(replaced_name, replacement_name)
- expect_to_execute_concurrently_in_order(drop_index)
+ expect_index_drop(drop_index)
subject.perform
@@ -166,9 +170,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
end
it 'replaces the existing index with an identical index' do
- expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').exactly(3).times
-
- expect_to_execute_concurrently_in_order(drop_index)
+ expect_index_drop(drop_index)
expect_to_execute_concurrently_in_order(create_index)
expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
@@ -179,7 +181,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
expect_index_rename(replacement_name, index.name)
expect_index_rename(replaced_name, replacement_name)
- expect_to_execute_concurrently_in_order(drop_index)
+ expect_index_drop(drop_index)
subject.perform
@@ -192,6 +194,10 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
expect(connection).to receive(:execute).with(create_index).ordered
.and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: false).and_yield
+ end
+
expect_to_execute_concurrently_in_order(drop_index)
expect { subject.perform }.to raise_error(ActiveRecord::ConnectionTimeoutError, /connect timeout/)
@@ -207,6 +213,10 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
expect_to_execute_concurrently_in_order(create_index)
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: false).and_yield
+ end
+
expect_to_execute_concurrently_in_order(drop_index)
expect { subject.perform }.to raise_error(described_class::ReindexError, /replacement index was created as INVALID/)
@@ -228,7 +238,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
expect_index_rename(index.name, replaced_name).and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout')
- expect_to_execute_concurrently_in_order(drop_index)
+ expect_index_drop(drop_index)
expect { subject.perform }.to raise_error(ActiveRecord::ConnectionTimeoutError, /connect timeout/)
@@ -245,7 +255,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
.and_raise(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, 'exhausted')
end
- expect_to_execute_concurrently_in_order(drop_index)
+ expect_index_drop(drop_index)
expect { subject.perform }.to raise_error(::Gitlab::Database::WithLockRetries::AttemptsExhaustedError, /exhausted/)
@@ -270,6 +280,14 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
SQL
end
+ def expect_index_drop(drop_index)
+ expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance|
+ expect(instance).to receive(:run).with(raise_on_exhaustion: false).and_yield
+ end
+
+ expect_to_execute_concurrently_in_order(drop_index)
+ end
+
def find_index_create_statement
ActiveRecord::Base.connection.select_value(<<~SQL)
SELECT indexdef
diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
new file mode 100644
index 00000000000..e93d8ab590d
--- /dev/null
+++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
@@ -0,0 +1,244 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
+ let(:env) { {} }
+ let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
+ let(:subject) { described_class.new(env: env, logger: logger, timing_configuration: timing_configuration) }
+
+ let(:timing_configuration) do
+ [
+ [1.second, 1.second],
+ [1.second, 1.second],
+ [1.second, 1.second],
+ [1.second, 1.second],
+ [1.second, 1.second]
+ ]
+ end
+
+ describe '#run' do
+ it 'requires block' do
+ expect { subject.run }.to raise_error(StandardError, 'no block given')
+ end
+
+ context 'when DISABLE_LOCK_RETRIES is set' do
+ let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } }
+
+ it 'executes the passed block without retrying' do
+ object = double
+
+ expect(object).to receive(:method).once
+
+ subject.run { object.method }
+ end
+ end
+
+ context 'when lock retry is enabled' do
+ let(:lock_fiber) do
+ Fiber.new do
+ # Initiating a second DB connection for the lock
+ conn = ActiveRecordSecond.establish_connection(Rails.configuration.database_configuration[Rails.env]).connection
+ conn.transaction do
+ conn.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+
+ Fiber.yield
+ end
+ ActiveRecordSecond.remove_connection # force disconnect
+ end
+ end
+
+ before do
+ stub_const('ActiveRecordSecond', Class.new(ActiveRecord::Base))
+
+ lock_fiber.resume # start the transaction and lock the table
+ end
+
+ after do
+ lock_fiber.resume if lock_fiber.alive?
+ end
+
+ context 'lock_fiber' do
+ it 'acquires lock successfully' do
+ check_exclusive_lock_query = """
+ SELECT 1
+ FROM pg_locks l
+ JOIN pg_class t ON l.relation = t.oid
+ WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}'
+ """
+
+ expect(ActiveRecord::Base.connection.execute(check_exclusive_lock_query).to_a).to be_present
+ end
+ end
+
+ shared_examples 'retriable exclusive lock on `projects`' do
+ it 'succeeds executing the given block' do
+ lock_attempts = 0
+ lock_acquired = false
+
+ # the actual number of attempts to run_block_with_lock_timeout can never exceed the number of
+ # timings_configurations, so here we limit the retry_count if it exceeds that value
+ #
+ # also, there is no call to sleep after the final attempt, which is why it will always be one less
+ expected_runs_with_timeout = [retry_count, timing_configuration.size].min
+ expect(subject).to receive(:sleep).exactly(expected_runs_with_timeout - 1).times
+
+ expect(subject).to receive(:run_block_with_lock_timeout).exactly(expected_runs_with_timeout).times.and_wrap_original do |method|
+ lock_fiber.resume if lock_attempts == retry_count
+
+ method.call
+ end
+
+ subject.run do
+ lock_attempts += 1
+
+ if lock_attempts == retry_count # we reached the last retry iteration, if we kill the thread, the last try (no lock_timeout) will succeed
+ lock_fiber.resume
+ end
+
+ ActiveRecord::Base.transaction do
+ ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+ lock_acquired = true
+ end
+ end
+
+ expect(lock_attempts).to eq(retry_count)
+ expect(lock_acquired).to eq(true)
+ end
+ end
+
+ context 'after 3 iterations' do
+ it_behaves_like 'retriable exclusive lock on `projects`' do
+ let(:retry_count) { 4 }
+ end
+
+ context 'setting the idle transaction timeout' do
+ context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
+ it 'does not disable the idle transaction timeout' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
+ allow(subject).to receive(:run_block_with_lock_timeout).once
+
+ expect(subject).not_to receive(:disable_idle_in_transaction_timeout)
+
+ subject.run {}
+ end
+ end
+
+ context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do
+ it 'disables the idle transaction timeout so the code can sleep and retry' do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
+
+ n = 0
+ allow(subject).to receive(:run_block_with_lock_timeout).twice do
+ n += 1
+ raise(ActiveRecord::LockWaitTimeout) if n == 1
+ end
+
+ expect(subject).to receive(:disable_idle_in_transaction_timeout).once
+
+ subject.run {}
+ end
+ end
+ end
+ end
+
+ context 'after the retries are exhausted' do
+ let(:timing_configuration) do
+ [
+ [1.second, 1.second]
+ ]
+ end
+
+ it 'disables the lock_timeout' do
+ allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
+
+ expect(subject).to receive(:disable_lock_timeout)
+
+ subject.run {}
+ end
+ end
+
+ context 'after the retries, without setting lock_timeout' do
+ let(:retry_count) { timing_configuration.size + 1 }
+
+ it_behaves_like 'retriable exclusive lock on `projects`' do
+ before do
+ expect(subject).to receive(:run_block_without_lock_timeout).and_call_original
+ end
+ end
+ end
+
+ context 'after the retries, when requested to raise an error' do
+ let(:expected_attempts_with_timeout) { timing_configuration.size }
+ let(:retry_count) { timing_configuration.size + 1 }
+
+ it 'raises an error instead of waiting indefinitely for the lock' do
+ lock_attempts = 0
+ lock_acquired = false
+
+ expect(subject).to receive(:sleep).exactly(expected_attempts_with_timeout - 1).times
+ expect(subject).to receive(:run_block_with_lock_timeout).exactly(expected_attempts_with_timeout).times.and_call_original
+
+ expect do
+ subject.run(raise_on_exhaustion: true) do
+ lock_attempts += 1
+
+ ActiveRecord::Base.transaction do
+ ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+ lock_acquired = true
+ end
+ end
+ end.to raise_error(described_class::AttemptsExhaustedError)
+
+ expect(lock_attempts).to eq(retry_count - 1)
+ expect(lock_acquired).to eq(false)
+ end
+ end
+
+ context 'when statement timeout is reached' do
+ it 'raises StatementInvalid error' do
+ lock_acquired = false
+ ActiveRecord::Base.connection.execute("SET statement_timeout='100ms'")
+
+ expect do
+ subject.run do
+ ActiveRecord::Base.connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
+ lock_acquired = true
+ end
+ end.to raise_error(ActiveRecord::StatementInvalid)
+
+ expect(lock_acquired).to eq(false)
+ end
+ end
+ end
+ end
+
+ context 'restore local database variables' do
+ it do
+ expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW lock_timeout").to_a }
+ end
+
+ it do
+ expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
+ end
+ end
+
+ context 'casting durations correctly' do
+ let(:timing_configuration) { [[0.015.seconds, 0.025.seconds], [0.015.seconds, 0.025.seconds]] } # 15ms, 25ms
+
+ it 'executes `SET lock_timeout` using the configured timeout value in milliseconds' do
+ expect(ActiveRecord::Base.connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original
+
+ subject.run { }
+ end
+
+ it 'calls `sleep` after the first iteration fails, using the configured sleep time' do
+ expect(subject).to receive(:run_block_with_lock_timeout).and_raise(ActiveRecord::LockWaitTimeout).twice
+ expect(subject).to receive(:sleep).with(0.025)
+
+ subject.run { }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index 563399ff0d9..b08f39fc92a 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -76,14 +76,14 @@ RSpec.describe Gitlab::Database::WithLockRetries do
lock_attempts = 0
lock_acquired = false
- # the actual number of attempts to run_block_with_transaction can never exceed the number of
+ # the actual number of attempts to run_block_with_lock_timeout can never exceed the number of
# timings_configurations, so here we limit the retry_count if it exceeds that value
#
# also, there is no call to sleep after the final attempt, which is why it will always be one less
expected_runs_with_timeout = [retry_count, timing_configuration.size].min
expect(subject).to receive(:sleep).exactly(expected_runs_with_timeout - 1).times
- expect(subject).to receive(:run_block_with_transaction).exactly(expected_runs_with_timeout).times.and_wrap_original do |method|
+ expect(subject).to receive(:run_block_with_lock_timeout).exactly(expected_runs_with_timeout).times.and_wrap_original do |method|
lock_fiber.resume if lock_attempts == retry_count
method.call
@@ -116,8 +116,8 @@ RSpec.describe Gitlab::Database::WithLockRetries do
context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
it 'does not disable the idle transaction timeout' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
- allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout)
- allow(subject).to receive(:run_block_with_transaction).once
+ allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
+ allow(subject).to receive(:run_block_with_lock_timeout).once
expect(subject).not_to receive(:disable_idle_in_transaction_timeout)
@@ -130,7 +130,7 @@ RSpec.describe Gitlab::Database::WithLockRetries do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
n = 0
- allow(subject).to receive(:run_block_with_transaction).twice do
+ allow(subject).to receive(:run_block_with_lock_timeout).twice do
n += 1
raise(ActiveRecord::LockWaitTimeout) if n == 1
end
@@ -153,7 +153,7 @@ RSpec.describe Gitlab::Database::WithLockRetries do
context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
it 'does not disable the lock_timeout' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
- allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout)
+ allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
expect(subject).not_to receive(:disable_lock_timeout)
@@ -164,7 +164,7 @@ RSpec.describe Gitlab::Database::WithLockRetries do
context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do
it 'disables the lock_timeout' do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
- allow(subject).to receive(:run_block_with_transaction).once.and_raise(ActiveRecord::LockWaitTimeout)
+ allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
expect(subject).to receive(:disable_lock_timeout)
@@ -192,7 +192,7 @@ RSpec.describe Gitlab::Database::WithLockRetries do
lock_acquired = false
expect(subject).to receive(:sleep).exactly(expected_attempts_with_timeout - 1).times
- expect(subject).to receive(:run_block_with_transaction).exactly(expected_attempts_with_timeout).times.and_call_original
+ expect(subject).to receive(:run_block_with_lock_timeout).exactly(expected_attempts_with_timeout).times.and_call_original
expect do
subject.run(raise_on_exhaustion: true) do
@@ -251,7 +251,7 @@ RSpec.describe Gitlab::Database::WithLockRetries do
end
it 'calls `sleep` after the first iteration fails, using the configured sleep time' do
- expect(subject).to receive(:run_block_with_transaction).and_raise(ActiveRecord::LockWaitTimeout).twice
+ expect(subject).to receive(:run_block_with_lock_timeout).and_raise(ActiveRecord::LockWaitTimeout).twice
expect(subject).to receive(:sleep).with(0.025)
subject.run { }
diff --git a/spec/lib/gitlab/pages/settings_spec.rb b/spec/lib/gitlab/pages/settings_spec.rb
index c89bf9ff206..1a7c808d1bf 100644
--- a/spec/lib/gitlab/pages/settings_spec.rb
+++ b/spec/lib/gitlab/pages/settings_spec.rb
@@ -47,12 +47,4 @@ RSpec.describe Gitlab::Pages::Settings do
end
end
end
-
- describe '#local_store' do
- subject(:local_store) { described_class.new(settings).local_store }
-
- it 'is an instance of Gitlab::Pages::Stores::LocalStore' do
- expect(local_store).to be_a(Gitlab::Pages::Stores::LocalStore)
- end
- end
end
diff --git a/spec/lib/gitlab/pages/stores/local_store_spec.rb b/spec/lib/gitlab/pages/stores/local_store_spec.rb
deleted file mode 100644
index adab81b2589..00000000000
--- a/spec/lib/gitlab/pages/stores/local_store_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Pages::Stores::LocalStore do
- describe '#enabled' do
- let(:local_store) { double(enabled: true) }
-
- subject(:local_store_enabled) { described_class.new(local_store).enabled }
-
- context 'when the pages_update_legacy_storage FF is disabled' do
- before do
- stub_feature_flags(pages_update_legacy_storage: false)
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'when the pages_update_legacy_storage FF is enabled' do
- it 'is equal to the original value' do
- expect(local_store_enabled).to eq(local_store.enabled)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 306537b69d1..b11cc6d8f6e 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1466,6 +1466,86 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
+ describe '.email_campaign_counts' do
+ subject { described_class.send(:email_campaign_counts) }
+
+ context 'when queries time out' do
+ before do
+ allow_any_instance_of(ActiveRecord::Relation)
+ .to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
+ end
+
+ it 'returns -1 for email campaign data' do
+ expected_data = {
+ "in_product_marketing_email_create_0_sent" => -1,
+ "in_product_marketing_email_create_0_cta_clicked" => -1,
+ "in_product_marketing_email_create_1_sent" => -1,
+ "in_product_marketing_email_create_1_cta_clicked" => -1,
+ "in_product_marketing_email_create_2_sent" => -1,
+ "in_product_marketing_email_create_2_cta_clicked" => -1,
+ "in_product_marketing_email_verify_0_sent" => -1,
+ "in_product_marketing_email_verify_0_cta_clicked" => -1,
+ "in_product_marketing_email_verify_1_sent" => -1,
+ "in_product_marketing_email_verify_1_cta_clicked" => -1,
+ "in_product_marketing_email_verify_2_sent" => -1,
+ "in_product_marketing_email_verify_2_cta_clicked" => -1,
+ "in_product_marketing_email_trial_0_sent" => -1,
+ "in_product_marketing_email_trial_0_cta_clicked" => -1,
+ "in_product_marketing_email_trial_1_sent" => -1,
+ "in_product_marketing_email_trial_1_cta_clicked" => -1,
+ "in_product_marketing_email_trial_2_sent" => -1,
+ "in_product_marketing_email_trial_2_cta_clicked" => -1,
+ "in_product_marketing_email_team_0_sent" => -1,
+ "in_product_marketing_email_team_0_cta_clicked" => -1,
+ "in_product_marketing_email_team_1_sent" => -1,
+ "in_product_marketing_email_team_1_cta_clicked" => -1,
+ "in_product_marketing_email_team_2_sent" => -1,
+ "in_product_marketing_email_team_2_cta_clicked" => -1
+ }
+
+ expect(subject).to eq(expected_data)
+ end
+ end
+
+ context 'when there are entries' do
+ before do
+ create(:in_product_marketing_email, track: :create, series: 0, cta_clicked_at: Time.zone.now)
+ create(:in_product_marketing_email, track: :verify, series: 0)
+ end
+
+ it 'gathers email campaign data' do
+ expected_data = {
+ "in_product_marketing_email_create_0_sent" => 1,
+ "in_product_marketing_email_create_0_cta_clicked" => 1,
+ "in_product_marketing_email_create_1_sent" => 0,
+ "in_product_marketing_email_create_1_cta_clicked" => 0,
+ "in_product_marketing_email_create_2_sent" => 0,
+ "in_product_marketing_email_create_2_cta_clicked" => 0,
+ "in_product_marketing_email_verify_0_sent" => 1,
+ "in_product_marketing_email_verify_0_cta_clicked" => 0,
+ "in_product_marketing_email_verify_1_sent" => 0,
+ "in_product_marketing_email_verify_1_cta_clicked" => 0,
+ "in_product_marketing_email_verify_2_sent" => 0,
+ "in_product_marketing_email_verify_2_cta_clicked" => 0,
+ "in_product_marketing_email_trial_0_sent" => 0,
+ "in_product_marketing_email_trial_0_cta_clicked" => 0,
+ "in_product_marketing_email_trial_1_sent" => 0,
+ "in_product_marketing_email_trial_1_cta_clicked" => 0,
+ "in_product_marketing_email_trial_2_sent" => 0,
+ "in_product_marketing_email_trial_2_cta_clicked" => 0,
+ "in_product_marketing_email_team_0_sent" => 0,
+ "in_product_marketing_email_team_0_cta_clicked" => 0,
+ "in_product_marketing_email_team_1_sent" => 0,
+ "in_product_marketing_email_team_1_cta_clicked" => 0,
+ "in_product_marketing_email_team_2_sent" => 0,
+ "in_product_marketing_email_team_2_cta_clicked" => 0
+ }
+
+ expect(subject).to eq(expected_data)
+ end
+ end
+ end
+
describe '.snowplow_event_counts' do
let_it_be(:time_period) { { collector_tstamp: 8.days.ago..1.day.ago } }
diff --git a/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
new file mode 100644
index 00000000000..3e57ffb4729
--- /dev/null
+++ b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20210430134202_copy_adoption_snapshot_namespace.rb')
+
+RSpec.describe CopyAdoptionSnapshotNamespace, :migration do
+ let(:namespaces_table) { table(:namespaces) }
+ let(:segments_table) { table(:analytics_devops_adoption_segments) }
+ let(:snapshots_table) { table(:analytics_devops_adoption_snapshots) }
+
+ before do
+ namespaces_table.create!(id: 123, name: 'group1', path: 'group1')
+ namespaces_table.create!(id: 124, name: 'group2', path: 'group2')
+
+ segments_table.create!(id: 1, namespace_id: 123)
+ segments_table.create!(id: 2, namespace_id: 124)
+
+ create_snapshot(id: 1, segment_id: 1)
+ create_snapshot(id: 2, segment_id: 2)
+ create_snapshot(id: 3, segment_id: 2, namespace_id: 123)
+ end
+
+ it 'updates all snapshots without namespace set' do
+ migrate!
+
+ expect(snapshots_table.find(1).namespace_id).to eq 123
+ expect(snapshots_table.find(2).namespace_id).to eq 124
+ expect(snapshots_table.find(3).namespace_id).to eq 123
+ end
+
+ def create_snapshot(**additional_params)
+ defaults = {
+ recorded_at: Time.zone.now,
+ issue_opened: true,
+ merge_request_opened: true,
+ merge_request_approved: true,
+ runner_configured: true,
+ pipeline_succeeded: true,
+ deploy_succeeded: true,
+ security_scan_succeeded: true,
+ end_time: Time.zone.now.end_of_month
+ }
+
+ snapshots_table.create!(defaults.merge(additional_params))
+ end
+end
diff --git a/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
new file mode 100644
index 00000000000..a37772db28c
--- /dev/null
+++ b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20210430135954_copy_adoption_segments_namespace.rb')
+
+RSpec.describe CopyAdoptionSegmentsNamespace, :migration do
+ let(:namespaces_table) { table(:namespaces) }
+ let(:segments_table) { table(:analytics_devops_adoption_segments) }
+
+ before do
+ namespaces_table.create!(id: 123, name: 'group1', path: 'group1')
+ namespaces_table.create!(id: 124, name: 'group2', path: 'group2')
+
+ segments_table.create!(id: 1, namespace_id: 123, display_namespace_id: nil)
+ segments_table.create!(id: 2, namespace_id: 124, display_namespace_id: 123)
+ end
+
+ it 'updates all segments without display namespace' do
+ migrate!
+
+ expect(segments_table.find(1).display_namespace_id).to eq 123
+ expect(segments_table.find(2).display_namespace_id).to eq 123
+ end
+end
diff --git a/spec/models/project_services/chat_message/alert_message_spec.rb b/spec/models/integrations/chat_message/alert_message_spec.rb
index 4d400990789..9866b2d9185 100644
--- a/spec/models/project_services/chat_message/alert_message_spec.rb
+++ b/spec/models/integrations/chat_message/alert_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::AlertMessage do
+RSpec.describe Integrations::ChatMessage::AlertMessage do
subject { described_class.new(args) }
let_it_be(:start_time) { Time.current }
diff --git a/spec/models/project_services/chat_message/base_message_spec.rb b/spec/models/integrations/chat_message/base_message_spec.rb
index a7ddf230758..eada5d1031d 100644
--- a/spec/models/project_services/chat_message/base_message_spec.rb
+++ b/spec/models/integrations/chat_message/base_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::BaseMessage do
+RSpec.describe Integrations::ChatMessage::BaseMessage do
let(:base_message) { described_class.new(args) }
let(:args) { { project_url: 'https://gitlab-domain.com' } }
diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/integrations/chat_message/deployment_message_spec.rb
index aa4ad54f9d5..ff255af11a3 100644
--- a/spec/models/project_services/chat_message/deployment_message_spec.rb
+++ b/spec/models/integrations/chat_message/deployment_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::DeploymentMessage do
+RSpec.describe Integrations::ChatMessage::DeploymentMessage do
describe '#pretext' do
it 'returns a message with the data returned by the deployment data builder' do
environment = create(:environment, name: "myenvironment")
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/integrations/chat_message/issue_message_spec.rb
index 4701ef3e49e..31b80ad3169 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/integrations/chat_message/issue_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::IssueMessage do
+RSpec.describe Integrations::ChatMessage::IssueMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/integrations/chat_message/merge_message_spec.rb
index 71cfe3ff45b..ed1ad6837e2 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/integrations/chat_message/merge_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::MergeMessage do
+RSpec.describe Integrations::ChatMessage::MergeMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/integrations/chat_message/note_message_spec.rb
index 6a741365d55..668c0da26ae 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/integrations/chat_message/note_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::NoteMessage do
+RSpec.describe Integrations::ChatMessage::NoteMessage do
subject { described_class.new(args) }
let(:color) { '#345' }
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/integrations/chat_message/pipeline_message_spec.rb
index 4eb2f57315b..a80d13d7f5d 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/integrations/chat_message/pipeline_message_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe ChatMessage::PipelineMessage do
+RSpec.describe Integrations::ChatMessage::PipelineMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/integrations/chat_message/push_message_spec.rb
index e3ba4c2aefe..167487449c3 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/integrations/chat_message/push_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::PushMessage do
+RSpec.describe Integrations::ChatMessage::PushMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
index 04c9e5934be..e8672a0f9c8 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatMessage::WikiPageMessage do
+RSpec.describe Integrations::ChatMessage::WikiPageMessage do
subject { described_class.new(args) }
let(:args) do
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 80528b14c39..52ef61e3d44 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -606,22 +606,6 @@ RSpec.describe Packages::Package, type: :model do
end
end
- describe '.processed' do
- let!(:package1) { create(:nuget_package) }
- let!(:package2) { create(:npm_package) }
- let!(:package3) { create(:nuget_package) }
-
- subject { described_class.processed }
-
- it { is_expected.to match_array([package1, package2, package3]) }
-
- context 'with temporary packages' do
- let!(:package1) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
-
- it { is_expected.to match_array([package2, package3]) }
- end
- end
-
describe '.limit_recent' do
let!(:package1) { create(:nuget_package) }
let!(:package2) { create(:nuget_package) }
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index e520f82c118..735f2225c21 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -52,11 +52,8 @@ RSpec.describe Pages::LookupPath do
expect(source[:path]).to eq(project.full_path + "/public/")
end
- it 'return nil when legacy storage is disabled and there is no deployment' do
- stub_feature_flags(pages_serve_from_legacy_storage: false)
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(described_class::LegacyStorageDisabledError, project_id: project.id)
- .and_call_original
+ it 'return nil when local storage is disabled and there is no deployment' do
+ allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
expect(source).to eq(nil)
end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index b7eb558b2e7..5f3a94a5b99 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -240,7 +240,7 @@ RSpec.describe MicrosoftTeamsService do
chat_service.execute(data)
- message = ChatMessage::PipelineMessage.new(data)
+ message = Integrations::ChatMessage::PipelineMessage.new(data)
expect(WebMock).to have_requested(:post, webhook_url)
.with(body: hash_including({ summary: message.summary }))
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
index 91bff49b239..215df81de63 100644
--- a/spec/services/spam/spam_verdict_service_spec.rb
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -114,6 +114,33 @@ RSpec.describe Spam::SpamVerdictService do
end
end
end
+
+ context 'records metrics' do
+ let(:histogram) { instance_double(Prometheus::Client::Histogram) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:verdict, :error, :label) do
+ Spam::SpamConstants::ALLOW | false | 'ALLOW'
+ Spam::SpamConstants::ALLOW | true | 'ERROR'
+ Spam::SpamConstants::CONDITIONAL_ALLOW | false | 'CONDITIONAL_ALLOW'
+ Spam::SpamConstants::BLOCK_USER | false | 'BLOCK'
+ Spam::SpamConstants::DISALLOW | false | 'DISALLOW'
+ Spam::SpamConstants::NOOP | false | 'NOOP'
+ end
+
+ with_them do
+ before do
+ allow(Gitlab::Metrics).to receive(:histogram).with(:gitlab_spamcheck_request_duration_seconds, anything).and_return(histogram)
+ allow(service).to receive(:spamcheck_verdict).and_return([verdict, attribs, error])
+ end
+
+ it 'records duration with labels' do
+ expect(histogram).to receive(:observe).with(a_hash_including(result: label), anything)
+ subject
+ end
+ end
+ end
end
describe '#akismet_verdict' do
@@ -313,7 +340,7 @@ RSpec.describe Spam::SpamVerdictService do
end
it 'returns nil' do
- expect(subject).to eq([ALLOW, attribs])
+ expect(subject).to eq([ALLOW, attribs, true])
end
end
@@ -335,7 +362,7 @@ RSpec.describe Spam::SpamVerdictService do
end
it 'returns nil' do
- expect(subject).to eq([ALLOW, attribs])
+ expect(subject).to eq([ALLOW, attribs, true])
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 34f9d189d71..2cc3e515d1d 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -269,9 +269,10 @@ RSpec.configure do |config|
stub_feature_flags(unified_diff_components: false)
- # The following `vue_issues_list` stub can be removed once the
- # Vue issues page has feature parity with the current Haml page
+ # The following `vue_issues_list`/`vue_issuables_list` stubs can be removed
+ # once the Vue issues page has feature parity with the current Haml page
stub_feature_flags(vue_issues_list: false)
+ stub_feature_flags(vue_issuables_list: false)
# Disable `refactor_blob_viewer` as we refactor
# the blob viewer. See the follwing epic for more:
diff --git a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
index 0e9e978fbd9..e776c098fa0 100644
--- a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
shared_examples_for 'service deleting todos' do
- before do
- stub_feature_flags(destroy_issuable_todos_async: group)
- end
-
it 'destroys associated todos asynchronously' do
expect(TodosDestroyer::DestroyedIssuableWorker)
.to receive(:perform_async)
@@ -12,29 +8,9 @@ shared_examples_for 'service deleting todos' do
subject.execute(issuable)
end
-
- context 'when destroy_issuable_todos_async feature is disabled for group' do
- before do
- stub_feature_flags(destroy_issuable_todos_async: false)
- end
-
- it 'destroy associated todos synchronously' do
- expect_next_instance_of(TodosDestroyer::DestroyedIssuableWorker) do |worker|
- expect(worker)
- .to receive(:perform)
- .with(issuable.id, issuable.class.name)
- end
-
- subject.execute(issuable)
- end
- end
end
shared_examples_for 'service deleting label links' do
- before do
- stub_feature_flags(destroy_issuable_label_links_async: group)
- end
-
it 'destroys associated label links asynchronously' do
expect(Issuable::LabelLinksDestroyWorker)
.to receive(:perform_async)
@@ -42,20 +18,4 @@ shared_examples_for 'service deleting label links' do
subject.execute(issuable)
end
-
- context 'when destroy_issuable_label_links_async feature is disabled for group' do
- before do
- stub_feature_flags(destroy_issuable_label_links_async: false)
- end
-
- it 'destroy associated label links synchronously' do
- expect_next_instance_of(Issuable::LabelLinksDestroyWorker) do |worker|
- expect(worker)
- .to receive(:perform)
- .with(issuable.id, issuable.class.name)
- end
-
- subject.execute(issuable)
- end
- end
end
diff --git a/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb b/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb
index 6c25eba03b9..6d56145144f 100644
--- a/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb
+++ b/spec/views/notify/change_in_merge_request_draft_status_email.html.haml_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe 'notify/change_in_merge_request_draft_status_email.html.haml' do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
+ let(:merge_request_link) { merge_request_url(merge_request) }
before do
assign(:updated_by_user, user)
@@ -15,5 +16,7 @@ RSpec.describe 'notify/change_in_merge_request_draft_status_email.html.haml' do
render
expect(rendered).to have_content("#{user.name} changed the draft status of merge request #{merge_request.to_reference}")
+ expect(rendered).to have_link(user.name, href: user_url(user))
+ expect(rendered).to have_link(merge_request.to_reference, href: merge_request_link)
end
end