summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_manual_todo.yml4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue3
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue50
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue51
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue7
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js1
-rw-r--r--app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue23
-rw-r--r--app/controllers/projects/commit_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/search_controller.rb6
-rw-r--r--app/helpers/sidebars_helper.rb3
-rw-r--r--app/helpers/workhorse_helper.rb12
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/concerns/milestoneish.rb2
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/sidebars/projects/menus/learn_gitlab/menu.rb41
-rw-r--r--app/models/sidebars/projects/panel.rb1
-rw-r--r--app/models/user.rb2
-rw-r--r--app/serializers/environment_serializer.rb54
-rw-r--r--app/services/ci/abort_pipelines_service.rb2
-rw-r--r--app/services/ci/drop_pipeline_service.rb37
-rw-r--r--app/views/admin/runners/show.html.haml18
-rw-r--r--app/views/groups/runners/edit.html.haml9
-rw-r--r--app/views/layouts/nav/sidebar/_project_menus.html.haml8
-rw-r--r--app/views/projects/runners/_runner.html.haml66
-rw-r--r--app/views/projects/runners/edit.html.haml11
-rw-r--r--app/views/shared/runners/_runner_type_alert.html.haml20
-rw-r--r--app/views/shared/runners/_runner_type_badge.html.haml10
-rw-r--r--app/views/shared/runners/show.html.haml15
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/ci/drop_pipeline_worker.rb16
-rwxr-xr-xbin/changelog2
-rwxr-xr-xbin/feature-flag2
-rw-r--r--changelogs/unreleased/244694-replace-gldeprecatedskeletonloading-with-glskeletonloader-in-app-a.yml5
-rw-r--r--changelogs/unreleased/323676-add-fk-to-partitioned-web-hook-logs.yml5
-rw-r--r--changelogs/unreleased/323714-add-blob-filename-to-attachment-content-dispostion.yml5
-rw-r--r--changelogs/unreleased/324786-update-deprecated-glicon-size-and-remove-use-deprecated-sizes.yml5
-rw-r--r--changelogs/unreleased/326197-project-runners-identifiers.yml5
-rw-r--r--changelogs/unreleased/327106-reduce-timeout-on-search-count.yml5
-rw-r--r--changelogs/unreleased/327199-consistently-indicate-runner-type-using-the-alert-component.yml5
-rw-r--r--changelogs/unreleased/deprecate_alerts_managed_prometheus.yml5
-rw-r--r--changelogs/unreleased/drop-pipelines-async-when-user-blocked.yml5
-rw-r--r--changelogs/unreleased/exclude-projects-dropdown-from-revert.yml5
-rw-r--r--changelogs/unreleased/id-enable-pick-into-project.yml5
-rw-r--r--changelogs/unreleased/jl-lower-milestone-issue-display-limit.yml5
-rw-r--r--changelogs/unreleased/mo-remove-codequality-backend-ff.yml5
-rw-r--r--changelogs/unreleased/optimize-environments-serializer.yml5
-rw-r--r--changelogs/unreleased/re-enable-pages-serving-zip-from-disk.yml5
-rw-r--r--config/feature_flags/development/attachment_with_filename.yml8
-rw-r--r--config/feature_flags/development/codequality_backend_comparison.yml8
-rw-r--r--config/feature_flags/development/pages_serve_with_zip_file_protocol.yml2
-rw-r--r--config/feature_flags/development/pick_into_project.yml2
-rw-r--r--data/whats_new/202008180003_13_01.yml6
-rw-r--r--data/whats_new/202009150001_13_03.yml2
-rw-r--r--data/whats_new/202011230001_13_06.yml4
-rw-r--r--data/whats_new/202012160001_13_07.yml2
-rw-r--r--data/whats_new/202103220001_13_10.yml6
-rw-r--r--data/whats_new/templates/YYYYMMDD0001_XX_YY.yml2
-rw-r--r--db/post_migrate/20210413130011_add_partitioned_web_hook_log_fk.rb22
-rw-r--r--db/schema_migrations/202104131300111
-rw-r--r--db/structure.sql3
-rw-r--r--doc/administration/instance_limits.md7
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/jobs.md80
-rw-r--r--doc/ci/services/gitlab.md44
-rw-r--r--doc/ci/services/index.md1
-rw-r--r--doc/development/changelog.md3
-rw-r--r--doc/development/deprecation_guidelines/index.md6
-rw-r--r--doc/development/usage_ping/dictionary.md4
-rw-r--r--doc/operations/metrics/alerts.md4
-rw-r--r--doc/user/application_security/dast/index.md6
-rw-r--r--doc/user/group/index.md6
-rw-r--r--doc/user/packages/dependency_proxy/index.md5
-rw-r--r--doc/user/project/issues/sorting_issue_lists.md6
-rw-r--r--doc/user/project/merge_requests/code_quality.md5
-rw-r--r--doc/user/project/quick_actions.md5
-rw-r--r--doc/user/project/time_tracking.md15
-rw-r--r--lib/api/entities/clusters/agent.rb12
-rw-r--r--lib/api/entities/job_request/job_info.rb2
-rw-r--r--lib/api/jobs.rb30
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb446
-rw-r--r--lib/banzai/filter/alert_reference_filter.rb29
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb46
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb86
-rw-r--r--lib/banzai/filter/design_reference_filter.rb107
-rw-r--r--lib/banzai/filter/epic_reference_filter.rb22
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb118
-rw-r--r--lib/banzai/filter/feature_flag_reference_filter.rb33
-rw-r--r--lib/banzai/filter/issuable_reference_filter.rb19
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb57
-rw-r--r--lib/banzai/filter/iteration_reference_filter.rb16
-rw-r--r--lib/banzai/filter/label_reference_filter.rb129
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb97
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb138
-rw-r--r--lib/banzai/filter/project_reference_filter.rb117
-rw-r--r--lib/banzai/filter/reference_filter.rb215
-rw-r--r--lib/banzai/filter/references/abstract_reference_filter.rb448
-rw-r--r--lib/banzai/filter/references/alert_reference_filter.rb31
-rw-r--r--lib/banzai/filter/references/commit_range_reference_filter.rb48
-rw-r--r--lib/banzai/filter/references/commit_reference_filter.rb88
-rw-r--r--lib/banzai/filter/references/design_reference_filter.rb109
-rw-r--r--lib/banzai/filter/references/epic_reference_filter.rb24
-rw-r--r--lib/banzai/filter/references/external_issue_reference_filter.rb120
-rw-r--r--lib/banzai/filter/references/feature_flag_reference_filter.rb35
-rw-r--r--lib/banzai/filter/references/issuable_reference_filter.rb21
-rw-r--r--lib/banzai/filter/references/issue_reference_filter.rb59
-rw-r--r--lib/banzai/filter/references/iteration_reference_filter.rb18
-rw-r--r--lib/banzai/filter/references/label_reference_filter.rb131
-rw-r--r--lib/banzai/filter/references/merge_request_reference_filter.rb99
-rw-r--r--lib/banzai/filter/references/milestone_reference_filter.rb140
-rw-r--r--lib/banzai/filter/references/project_reference_filter.rb119
-rw-r--r--lib/banzai/filter/references/reference_filter.rb217
-rw-r--r--lib/banzai/filter/references/snippet_reference_filter.rb31
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb182
-rw-r--r--lib/banzai/filter/references/vulnerability_reference_filter.rb24
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb29
-rw-r--r--lib/banzai/filter/user_reference_filter.rb180
-rw-r--r--lib/banzai/filter/vulnerability_reference_filter.rb22
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb26
-rw-r--r--lib/banzai/pipeline/label_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb18
-rw-r--r--lib/gitlab/ci/features.rb4
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb74
-rw-r--r--locale/gitlab.pot69
-rwxr-xr-xscripts/verify-tff-mapping2
-rw-r--r--spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb11
-rw-r--r--spec/db/schema_spec.rb3
-rw-r--r--spec/features/runners_spec.rb6
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js49
-rw-r--r--spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js30
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js6
-rw-r--r--spec/lib/api/entities/clusters/agent_spec.rb16
-rw-r--r--spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/abstract_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/alert_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/alert_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/commit_range_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/commit_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/commit_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/design_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/design_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/external_issue_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/issue_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/issue_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/label_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/label_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/merge_request_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/milestone_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/project_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/project_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/reference_filter_spec.rb (renamed from spec/lib/banzai/filter/reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/snippet_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/filter/references/user_reference_filter_spec.rb (renamed from spec/lib/banzai/filter/user_reference_filter_spec.rb)2
-rw-r--r--spec/lib/banzai/pipeline/gfm_pipeline_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb20
-rw-r--r--spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb31
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb3
-rw-r--r--spec/requests/api/jobs_spec.rb12
-rw-r--r--spec/serializers/environment_serializer_spec.rb50
-rw-r--r--spec/services/ci/drop_pipeline_service_spec.rb60
-rw-r--r--spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb29
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb10
-rw-r--r--spec/views/shared/runners/show.html.haml_spec.rb6
-rw-r--r--spec/workers/ci/drop_pipeline_worker_spec.rb36
-rwxr-xr-xworkhorse/_support/changelog2
166 files changed, 3069 insertions, 2306 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 109d13aa163..dc21c6a62d5 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -1363,8 +1363,8 @@ RSpec/AnyInstanceOf:
- 'spec/lib/backup/files_spec.rb'
- 'spec/lib/backup/manager_spec.rb'
- 'spec/lib/banzai/commit_renderer_spec.rb'
- - 'spec/lib/banzai/filter/external_issue_reference_filter_spec.rb'
- - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb'
+ - 'spec/lib/banzai/filter/references/issue_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/repository_link_filter_spec.rb'
- 'spec/lib/banzai/pipeline/gfm_pipeline_spec.rb'
- 'spec/lib/extracts_ref_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8c745bb4253..1f6f6697a5a 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-6904387a86815c80988d87f23af9d3fe1e2d4c85
+7c28c2a9821c44f3b2115d7fcf74a4816c18dd6c
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index 2169c9111d2..b6163491abc 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -137,8 +137,7 @@ export default {
<span :title="icon.tooltip" :aria-label="icon.tooltip">
<gl-icon
:name="icon.name"
- :size="18"
- use-deprecated-sizes
+ :size="16"
:class="icon.classes"
data-qa-selector="design_status_icon"
:data-qa-status="icon.name"
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index e6bd0f53672..aea4a8b1c0b 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -1,15 +1,8 @@
<script>
-import {
- GlFormGroup,
- GlFormCheckbox,
- GlFormInput,
- GlSprintf,
- GlLink,
- GlButton,
- GlCard,
-} from '@gitlab/ui';
+import { GlFormGroup, GlFormCheckbox, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
+import JiraUpgradeCta from './jira_upgrade_cta.vue';
export default {
name: 'JiraIssuesFields',
@@ -19,8 +12,7 @@ export default {
GlFormInput,
GlSprintf,
GlLink,
- GlButton,
- GlCard,
+ JiraUpgradeCta,
JiraIssueCreationVulnerabilities: () =>
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
@@ -84,11 +76,13 @@ export default {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
showJiraVulnerabilitiesOptions() {
- return (
- this.enableJiraIssues &&
- this.showJiraVulnerabilitiesIntegration &&
- this.glFeatures.jiraForVulnerabilities
- );
+ return this.showJiraVulnerabilitiesIntegration && this.glFeatures.jiraForVulnerabilities;
+ },
+ showUltimateUpgrade() {
+ return this.showJiraIssuesIntegration && !this.showJiraVulnerabilitiesIntegration;
+ },
+ showPremiumUpgrade() {
+ return !this.showJiraIssuesIntegration;
},
},
created() {
@@ -135,27 +129,23 @@ export default {
</template>
</gl-form-checkbox>
<jira-issue-creation-vulnerabilities
- v-if="showJiraVulnerabilitiesOptions"
+ v-if="enableJiraIssues"
:project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
+ :show-full-feature="showJiraVulnerabilitiesOptions"
data-testid="jira-for-vulnerabilities"
@request-get-issue-types="getJiraIssueTypes"
/>
</template>
- <gl-card v-else class="gl-mt-7">
- <strong>{{ __('This is a Premium feature') }}</strong>
- <p>{{ __('Upgrade your plan to enable this feature of the Jira Integration.') }}</p>
- <gl-button
- v-if="upgradePlanPath"
- category="primary"
- variant="info"
- :href="upgradePlanPath"
- target="_blank"
- >
- {{ __('Upgrade your plan') }}
- </gl-button>
- </gl-card>
+ <jira-upgrade-cta
+ v-if="showUltimateUpgrade || showPremiumUpgrade"
+ class="gl-mt-2"
+ :class="{ 'gl-ml-6': showUltimateUpgrade }"
+ :upgrade-plan-path="upgradePlanPath"
+ :show-ultimate-message="showUltimateUpgrade"
+ :show-premium-message="showPremiumUpgrade"
+ />
</div>
</gl-form-group>
<template v-if="showJiraIssuesIntegration">
diff --git a/app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue b/app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue
new file mode 100644
index 00000000000..9164e484440
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/jira_upgrade_cta.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlButton, GlCard } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlCard,
+ },
+ props: {
+ upgradePlanPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showPremiumMessage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showUltimateMessage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ title() {
+ return this.showUltimateMessage
+ ? this.$options.i18n.titleUltimate
+ : this.$options.i18n.titlePremium;
+ },
+ },
+ i18n: {
+ titleUltimate: s__('JiraService|This is an Ultimate feature'),
+ titlePremium: s__('JiraService|This is a Premium feature'),
+ content: s__('JiraService|Upgrade your plan to enable this feature of the Jira Integration.'),
+ upgrade: __('Upgrade your plan'),
+ },
+};
+</script>
+
+<template>
+ <gl-card>
+ <strong>{{ title }}</strong>
+ <p>{{ $options.i18n.content }}</p>
+ <gl-button v-if="upgradePlanPath" category="primary" variant="info" :href="upgradePlanPath">
+ {{ $options.i18n.upgrade }}
+ </gl-button>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index a5f26416828..6eefa5f55e4 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -37,6 +37,11 @@ export default {
type: String,
required: true,
},
+ isCherryPick: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -111,7 +116,7 @@ export default {
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-form-group
- v-if="glFeatures.pickIntoProject"
+ v-if="glFeatures.pickIntoProject && isCherryPick"
:label="i18n.projectLabel"
label-for="start_project"
data-testid="dropdown-group"
diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
index ad31ad14b2a..47ee8237fea 100644
--- a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js
@@ -51,6 +51,7 @@ export default function initInviteMembersModal() {
i18n: { ...I18N_CHERRY_PICK_MODAL, ...I18N_MODAL },
openModal: OPEN_CHERRY_PICK_MODAL,
modalId: CHERRY_PICK_MODAL_ID,
+ isCherryPick: true,
},
}),
});
diff --git a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
index 654508f0736..cfefdf82d85 100644
--- a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
+++ b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
@@ -62,7 +62,7 @@ export default {
helpPath: this.codequalityHelpPath,
});
- this.fetchReports(this.glFeatures.codequalityBackendComparison);
+ this.fetchReports();
},
methods: {
...mapActions(['fetchReports', 'setPaths']),
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 7182079860a..11f484b2cdf 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -1,11 +1,6 @@
<script>
/* eslint-disable vue/no-v-html */
-import {
- GlPopover,
- GlLink,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlIcon,
-} from '@gitlab/ui';
+import { GlPopover, GlLink, GlSkeletonLoader, GlIcon } from '@gitlab/ui';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import { glEmojiTag } from '../../../emoji';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
@@ -19,7 +14,7 @@ export default {
GlIcon,
GlLink,
GlPopover,
- GlSkeletonLoading,
+ GlSkeletonLoader,
UserAvatarImage,
UserNameWithStatus,
},
@@ -65,15 +60,13 @@ export default {
<div class="gl-p-2 flex-shrink-1">
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="gl-mr-3!" />
</div>
- <div class="gl-p-2 gl-w-full">
+ <div class="gl-p-2 gl-w-full gl-min-w-0">
<template v-if="userIsLoading">
- <!-- `gl-skeleton-loading` does not support equal length lines -->
- <!-- This can be migrated to `gl-skeleton-loader` when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/872 is completed -->
- <gl-skeleton-loading
- v-for="n in $options.maxSkeletonLines"
- :key="n"
- :lines="1"
- class="animation-container-small gl-mb-2"
+ <gl-skeleton-loader
+ :lines="$options.maxSkeletonLines"
+ preserve-aspect-ratio="none"
+ equal-width-lines
+ :height="52"
/>
</template>
<template v-else>
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 1e65974a3cd..3853797e0bf 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -20,7 +20,7 @@ class Projects::CommitController < Projects::ApplicationController
before_action :define_note_vars, only: [:show, :diff_for_path, :diff_files]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
before_action do
- push_frontend_feature_flag(:pick_into_project)
+ push_frontend_feature_flag(:pick_into_project, @project, default_enabled: :yaml)
end
BRANCH_SEARCH_LIMIT = 1000
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9081c9f7c57..4d612cd45d0 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -37,7 +37,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project, default_enabled: :yaml)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true)
- push_frontend_feature_flag(:codequality_backend_comparison, @project, default_enabled: :yaml)
push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 45c1c35a655..3b218822395 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -47,7 +47,11 @@ class SearchController < ApplicationController
params.require([:search, :scope])
scope = search_service.scope
- count = search_service.search_results.formatted_count(scope)
+
+ count = 0
+ ApplicationRecord.with_fast_read_statement_timeout do
+ count = search_service.search_results.formatted_count(scope)
+ end
# Users switching tabs will keep fetching the same tab counts so it's a
# good idea to cache in their browser just for a short time. They can still
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index efb0fd8a6e3..b0fffe93c25 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -35,7 +35,8 @@ module SidebarsHelper
def project_sidebar_context_data(project, user)
{
current_user: user,
- container: project
+ container: project,
+ learn_gitlab_experiment_enabled: learn_gitlab_experiment_enabled?(project)
}
end
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 28dd1b00292..8785c4cdcbb 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -7,7 +7,7 @@ module WorkhorseHelper
def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
- headers['Content-Disposition'] = inline ? 'inline' : content_disposition_attachment(repository.project, blob.name)
+ headers['Content-Disposition'] = content_disposition_for_blob(blob, inline)
# If enabled, this will override the values set above
workhorse_set_content_type!
@@ -49,11 +49,9 @@ module WorkhorseHelper
headers[Gitlab::Workhorse::DETECT_HEADER] = "true"
end
- def content_disposition_attachment(project, filename)
- if Feature.enabled?(:attachment_with_filename, project, default_enabled: :yaml)
- ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
- else
- 'attachment'
- end
+ def content_disposition_for_blob(blob, inline)
+ return 'inline' if inline
+
+ ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: blob.name)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 57f5f66891e..3d8e9f4c126 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -377,11 +377,11 @@ module Ci
end
def other_manual_actions
- pipeline.manual_actions.where.not(name: name)
+ pipeline.manual_actions.reject { |action| action.name == self.name }
end
def other_scheduled_actions
- pipeline.scheduled_actions.where.not(name: name)
+ pipeline.scheduled_actions.reject { |action| action.name == self.name }
end
def pages_generator?
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 8f7e6041d4b..eaf64f2541d 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Milestoneish
- DISPLAY_ISSUES_LIMIT = 3000
+ DISPLAY_ISSUES_LIMIT = 500
def total_issues_count
@total_issues_count ||= Milestones::IssuesCountService.new(self).count
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 0bfe6172154..d3280403bfd 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -171,7 +171,7 @@ class Deployment < ApplicationRecord
end
def commit
- project.commit(sha)
+ @commit ||= project.commit(sha)
end
def commit_title
@@ -250,7 +250,7 @@ class Deployment < ApplicationRecord
return unless on_stop.present?
return unless manual_actions
- @stop_action ||= manual_actions.find_by(name: on_stop)
+ @stop_action ||= manual_actions.find { |action| action.name == self.on_stop }
end
def finished_at
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 4cc65f4e295..4ee93b0ba4a 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -24,13 +24,13 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
- has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
+ has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
- has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment'
+ has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 8f2ec2d6b88..e7f3762b9a3 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -37,7 +37,7 @@ class MergeRequest < ApplicationRecord
SORTING_PREFERENCE_FIELD = :merge_requests_sort
ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = {
- 'Ci::CompareCodequalityReportsService' => ->(project) { ::Gitlab::Ci::Features.display_codequality_backend_comparison?(project) }
+ 'Ci::CompareCodequalityReportsService' => ->(project) { true }
}.freeze
belongs_to :target_project, class_name: "Project"
@@ -1564,8 +1564,6 @@ class MergeRequest < ApplicationRecord
end
def has_codequality_reports?
- return false unless ::Gitlab::Ci::Features.display_codequality_backend_comparison?(project)
-
actual_head_pipeline&.has_reports?(Ci::JobArtifact.codequality_reports)
end
diff --git a/app/models/sidebars/projects/menus/learn_gitlab/menu.rb b/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
new file mode 100644
index 00000000000..4b572846d1a
--- /dev/null
+++ b/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Projects
+ module Menus
+ module LearnGitlab
+ class Menu < ::Sidebars::Menu
+ override :link
+ def link
+ project_learn_gitlab_path(context.project)
+ end
+
+ override :active_routes
+ def active_routes
+ { controller: :learn_gitlab }
+ end
+
+ override :title
+ def title
+ _('Learn GitLab')
+ end
+
+ override :extra_container_html_options
+ def nav_link_html_options
+ { class: 'home' }
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'home'
+ end
+
+ override :render?
+ def render?
+ context.learn_gitlab_experiment_enabled
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb
index 5f4c7f32164..60cb804f5e8 100644
--- a/app/models/sidebars/projects/panel.rb
+++ b/app/models/sidebars/projects/panel.rb
@@ -8,6 +8,7 @@ module Sidebars
set_scope_menu(Sidebars::Projects::Menus::Scope::Menu.new(context))
add_menu(Sidebars::Projects::Menus::ProjectOverview::Menu.new(context))
+ add_menu(Sidebars::Projects::Menus::LearnGitlab::Menu.new(context))
end
override :render_raw_menus_partial
diff --git a/app/models/user.rb b/app/models/user.rb
index 426309762ca..507e8cc2cf5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -354,7 +354,7 @@ class User < ApplicationRecord
# this state transition object in order to do a rollback.
# For this reason the tradeoff is to disable this cop.
after_transition any => :blocked do |user|
- Ci::AbortPipelinesService.new.execute(user.pipelines, :user_blocked)
+ Ci::DropPipelineService.new.execute_async_for_all(user.pipelines, :user_blocked, user)
Ci::DisableUserPipelineSchedulesService.new.execute(user)
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index 598ce5f9e4f..2bb9a7e7254 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -23,7 +23,7 @@ class EnvironmentSerializer < BaseSerializer
latest: super(item.latest, opts) }
end
else
- super(resource, opts)
+ super(batch_load(resource), opts)
end
end
@@ -41,11 +41,59 @@ class EnvironmentSerializer < BaseSerializer
# immediately.
items = @paginator.paginate(items) if paginated?
- environments = resource.where(id: items.map(&:last_id)).index_by(&:id)
+ environments = batch_load(resource.where(id: items.map(&:last_id)))
+ environments_by_id = environments.index_by(&:id)
items.map do |item|
- Item.new(item.folder, item.size, environments[item.last_id])
+ Item.new(item.folder, item.size, environments_by_id[item.last_id])
end
end
+
+ def batch_load(resource)
+ resource = resource.preload(environment_associations)
+
+ resource.all.tap do |environments|
+ environments.each do |environment|
+ # Batch loading the commits of the deployments
+ environment.last_deployment&.commit&.try(:lazy_author)
+ environment.upcoming_deployment&.commit&.try(:lazy_author)
+ end
+ end
+ end
+
+ def environment_associations
+ {
+ last_deployment: deployment_associations,
+ upcoming_deployment: deployment_associations,
+ project: project_associations
+ }
+ end
+
+ def deployment_associations
+ {
+ user: [],
+ cluster: [],
+ project: [],
+ deployable: {
+ user: [],
+ metadata: [],
+ pipeline: {
+ manual_actions: [],
+ scheduled_actions: []
+ },
+ project: project_associations
+ }
+ }
+ end
+
+ def project_associations
+ {
+ project_feature: [],
+ route: [],
+ namespace: :route
+ }
+ end
# rubocop: enable CodeReuse/ActiveRecord
end
+
+EnvironmentSerializer.prepend_if_ee('EE::EnvironmentSerializer')
diff --git a/app/services/ci/abort_pipelines_service.rb b/app/services/ci/abort_pipelines_service.rb
index ad619dbdc41..43734c4dd39 100644
--- a/app/services/ci/abort_pipelines_service.rb
+++ b/app/services/ci/abort_pipelines_service.rb
@@ -3,7 +3,7 @@
module Ci
class AbortPipelinesService
# NOTE: This call fails pipelines in bulk without running callbacks.
- # Only for pipeline abandonment scenarios (examples: project delete, user block)
+ # Only for pipeline abandonment scenarios (examples: project delete)
def execute(pipelines, failure_reason)
pipelines.cancelable.each_batch(of: 100) do |pipeline_batch|
now = Time.current
diff --git a/app/services/ci/drop_pipeline_service.rb b/app/services/ci/drop_pipeline_service.rb
new file mode 100644
index 00000000000..f510943251b
--- /dev/null
+++ b/app/services/ci/drop_pipeline_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Ci
+ class DropPipelineService
+ # execute service asynchronously for each cancelable pipeline
+ def execute_async_for_all(pipelines, failure_reason, context_user)
+ pipelines.cancelable.select(:id).find_in_batches do |pipelines_batch|
+ Ci::DropPipelineWorker.bulk_perform_async_with_contexts(
+ pipelines_batch,
+ arguments_proc: -> (pipeline) { [pipeline.id, failure_reason] },
+ context_proc: -> (_) { { user: context_user } }
+ )
+ end
+ end
+
+ def execute(pipeline, failure_reason, retries: 3)
+ Gitlab::OptimisticLocking.retry_lock(pipeline.cancelable_statuses, retries, name: 'ci_pipeline_drop_running') do |cancelables|
+ cancelables.find_in_batches do |batch|
+ preload_associations_for_drop(batch)
+
+ batch.each do |job|
+ job.drop(failure_reason)
+ end
+ end
+ end
+ end
+
+ private
+
+ def preload_associations_for_drop(builds_batch)
+ ActiveRecord::Associations::Preloader.new.preload( # rubocop: disable CodeReuse/ActiveRecord
+ builds_batch,
+ [:project, :pipeline, :metadata, :deployment, :taggings]
+ )
+ end
+ end
+end
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index b5940fb41bc..705716c09b7 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -8,22 +8,10 @@
#js-runner-detail{ data: {runner_id: @runner.id} }
- else
%h2.page-title
- = sprintf(s_('Runners|Runner #%{runner_id}'), {runner_id: @runner.id})
+ = s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
+ = render 'shared/runners/runner_type_badge', runner: @runner
-- if @runner.instance_type?
- .bs-callout.bs-callout-success
- %h4= _('This runner processes jobs for all unassigned projects.')
- %p
- = _('If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner.')
-- elsif @runner.group_type?
- .bs-callout.bs-callout-success
- %h4= _('This runner processes jobs for all projects in its group and subgroups.')
-- else
- .bs-callout.bs-callout-info
- %h4= _('This runner processes jobs for assigned projects only.')
- %p
- = _('You cannot make this a shared runner.')
-%hr
+= render 'shared/runners/runner_type_alert', runner: @runner
.gl-mb-6
= render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner), in_gitlab_com_admin_context: Gitlab.com?
diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml
index c332009def4..3794c345aa6 100644
--- a/app/views/groups/runners/edit.html.haml
+++ b/app/views/groups/runners/edit.html.haml
@@ -1,6 +1,9 @@
- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('Runners')
-%h4 Runner ##{@runner.id}
+%h2.page-title
+ = s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
+ = render 'shared/runners/runner_type_badge', runner: @runner
-%hr
- = render 'shared/runners/form', runner: @runner, runner_form_url: group_runner_path(@group, @runner)
+= render 'shared/runners/runner_type_alert', runner: @runner
+
+= render 'shared/runners/form', runner: @runner, runner_form_url: group_runner_path(@group, @runner)
diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml
index aee506ead6c..8f2da398164 100644
--- a/app/views/layouts/nav/sidebar/_project_menus.html.haml
+++ b/app/views/layouts/nav/sidebar/_project_menus.html.haml
@@ -1,11 +1,3 @@
-- if project_nav_tab? :learn_gitlab
- = nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
- = link_to project_learn_gitlab_path(@project) do
- .nav-icon-container
- = sprite_icon('home')
- %span.nav-item-name
- = _('Learn GitLab')
-
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
= link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index bc043a5f394..bf2e746b4a4 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -1,44 +1,40 @@
-%li.runner{ id: dom_id(runner) }
- %h4.gl-font-weight-normal
- = runner_status_icon(runner, size: 16, icon_class: "gl-vertical-align-middle!")
-
- - if @project_runners.include?(runner)
- = link_to _("%{token}...") % { token: runner.short_sha }, project_runner_path(@project, runner), class: 'commit-sha has-tooltip', title: _("Partial token for reference only")
-
+%li{ id: dom_id(runner) }
+ .gl-display-flex.gl-justify-content-space-between
+ %div
+ = runner_status_icon(runner, size: 16)
+ - if @project_runners.include?(runner)
+ = link_to "##{runner.id} (#{runner.short_sha})", project_runner_path(@project, runner)
+ - else
+ %span
+ = "##{runner.id} (#{runner.short_sha})"
- if runner.locked?
%span.has-tooltip{ title: _('Locked to current projects') }
= sprite_icon('lock')
-
- = link_to edit_project_runner_path(@project, runner), class: 'btn gl-button btn-sm btn-icon', data: { testid: 'edit-runner-link' } do
- = sprite_icon('pencil')
-
- - else
- %span.commit-sha
- = runner.short_sha
-
- .float-right
- - if @project_runners.include?(runner)
- - if runner.active?
- = link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn gl-button btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- - else
- = link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn gl-button btn-confirm btn-sm'
- - if runner.belongs_to_one_project?
- = link_to _('Remove runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn gl-button btn-danger btn-sm'
- - else
- - runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord
- = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn gl-button btn-danger btn-sm'
- - elsif runner.project_type?
- = form_for [@project, @project.runner_projects.new] do |f|
- = f.hidden_field :runner_id, value: runner.id
- = f.submit _('Enable for this project'), class: 'btn gl-button btn-sm'
- .float-right
- %small.light
- \##{runner.id}
+ .gl-ml-2
+ .btn-group.btn-group-sm
+ - if @project_runners.include?(runner)
+ = link_to edit_project_runner_path(@project, runner), class: 'btn gl-button btn-icon', title: _('Edit'), aria: { label: _('Edit') }, data: { testid: 'edit-runner-link', toggle: 'tooltip', placement: 'top', container: 'body' } do
+ = sprite_icon('pencil')
+ - if runner.active?
+ = link_to pause_project_runner_path(@project, runner), method: :post, class: 'btn gl-button btn-icon', title: _('Pause'), aria: { label: _('Pause') }, data: { toggle: 'tooltip', placement: 'top', container: 'body', confirm: _("Are you sure?") } do
+ = sprite_icon('pause')
+ - else
+ = link_to resume_project_runner_path(@project, runner), method: :post, class: 'btn gl-button btn-icon', title: _('Resume'), aria: { label: _('Resume') }, data: { toggle: 'tooltip', placement: 'top', container: 'body' } do
+ = sprite_icon('play')
+ - if runner.belongs_to_one_project?
+ = link_to _('Remove runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn gl-button btn-danger'
+ - else
+ - runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord
+ = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn gl-button btn-danger'
+ - elsif runner.project_type?
+ = form_for [@project, @project.runner_projects.new] do |f|
+ = f.hidden_field :runner_id, value: runner.id
+ = f.submit _('Enable for this project'), class: 'btn gl-button'
- if runner.description.present?
- %p.runner-description
+ %p.gl-my-2
= runner.description
- if runner.tags.present?
- %p
+ .gl-my-2
- runner.tags.map(&:name).sort.each do |tag|
%span.badge.gl-badge.sm.badge-pill.badge-primary
= tag
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index f93cd23c83e..77150715158 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -1,6 +1,9 @@
-- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('runners')
+- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('Runners')
-%h4 Runner ##{@runner.id}
+%h2.page-title
+ = s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
+ = render 'shared/runners/runner_type_badge', runner: @runner
-%hr
- = render 'shared/runners/form', runner: @runner, runner_form_url: project_runner_path(@project, @runner)
+= render 'shared/runners/runner_type_alert', runner: @runner
+
+= render 'shared/runners/form', runner: @runner, runner_form_url: project_runner_path(@project, @runner)
diff --git a/app/views/shared/runners/_runner_type_alert.html.haml b/app/views/shared/runners/_runner_type_alert.html.haml
new file mode 100644
index 00000000000..b83def8b802
--- /dev/null
+++ b/app/views/shared/runners/_runner_type_alert.html.haml
@@ -0,0 +1,20 @@
+.gl-alert.gl-alert-info.gl-my-5
+ = sprite_icon('information-o', css_class: 'gl-alert-icon')
+ - if runner.instance_type?
+ %h4.gl-alert-title
+ = s_('Runners|This runner is available to all groups and projects in your GitLab instance.')
+ .gl-alert-body
+ = s_('Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner.')
+ = link_to _('Learn more.'), help_page_path('ci/runners/README', anchor: 'shared-runners'), target: '_blank', rel: 'noopener noreferrer'
+ - elsif runner.group_type?
+ %h4.gl-alert-title
+ = s_('Runners|This runner is available to all projects and subgroups in a group.')
+ .gl-alert-body
+ = s_('Runners|Use Group runners when you want all projects in a group to have access to a set of runners.')
+ = link_to _('Learn more.'), help_page_path('ci/runners/README', anchor: 'group-runners'), target: '_blank', rel: 'noopener noreferrer'
+ - else
+ %h4.gl-alert-title
+ = s_('Runners|This runner is associated with specific projects.')
+ .gl-alert-body
+ = s_('Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner.')
+ = link_to _('Learn more.'), help_page_path('ci/runners/README', anchor: 'specific-runners'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/shared/runners/_runner_type_badge.html.haml b/app/views/shared/runners/_runner_type_badge.html.haml
new file mode 100644
index 00000000000..e0318006f09
--- /dev/null
+++ b/app/views/shared/runners/_runner_type_badge.html.haml
@@ -0,0 +1,10 @@
+
+- if runner.instance_type?
+ %span.badge.badge-pill.gl-badge.badge-success
+ = s_('Runners|shared')
+- elsif runner.group_type?
+ %span.badge.badge-pill.gl-badge.badge-success
+ = s_('Runners|group')
+- else
+ %span.badge.badge-pill.gl-badge.badge-info
+ = s_('Runners|specific')
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index 1af04b808bf..757ec870f79 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -1,17 +1,8 @@
- page_title "#{@runner.description} ##{@runner.id}", _("Runners")
-%h3.page-title
- = s_('Runners|Runner #%{id}' % { id: @runner.id })
- .float-right
- - if @runner.instance_type?
- %span.runner-state.runner-state-shared
- = s_('Runners|Shared')
- - elsif @runner.group_type?
- %span.runner-state.runner-state-shared
- = s_('Runners|Group')
- - else
- %span.runner-state.runner-state-specific
- = s_('Runners|Specific')
+%h2.page-title
+ = s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
+ = render 'shared/runners/runner_type_badge', runner: @runner
.table-holder
%table.table
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f14497509e1..692adcb6434 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1251,6 +1251,14 @@
:weight: 3
:idempotent:
:tags: []
+- :name: pipeline_default:ci_drop_pipeline
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 3
+ :idempotent: true
+ :tags: []
- :name: pipeline_default:ci_merge_requests_add_todo_when_build_fails
:feature_category: :continuous_integration
:has_external_dependencies:
diff --git a/app/workers/ci/drop_pipeline_worker.rb b/app/workers/ci/drop_pipeline_worker.rb
new file mode 100644
index 00000000000..d19157a47e8
--- /dev/null
+++ b/app/workers/ci/drop_pipeline_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ci
+ class DropPipelineWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ idempotent!
+
+ def perform(pipeline_id, failure_reason)
+ Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
+ Ci::DropPipelineService.new.execute(pipeline, failure_reason.to_sym)
+ end
+ end
+ end
+end
diff --git a/bin/changelog b/bin/changelog
index bdf159a0a22..e796b1df600 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -71,7 +71,7 @@ class ChangelogOptionParser
options.force = value
end
- opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
+ opts.on('-m', '--merge-request [integer]', Integer, 'Merge request ID') do |value|
options.merge_request = value
end
diff --git a/bin/feature-flag b/bin/feature-flag
index 613ddc1d8cb..06387d12eb5 100755
--- a/bin/feature-flag
+++ b/bin/feature-flag
@@ -61,7 +61,7 @@ class FeatureFlagOptionParser
options.force = value
end
- opts.on('-m', '--introduced-by-url [string]', String, 'URL of Merge Request introducing the Feature Flag') do |value|
+ opts.on('-m', '--introduced-by-url [string]', String, 'URL of merge request introducing the Feature Flag') do |value|
options.introduced_by_url = value
end
diff --git a/changelogs/unreleased/244694-replace-gldeprecatedskeletonloading-with-glskeletonloader-in-app-a.yml b/changelogs/unreleased/244694-replace-gldeprecatedskeletonloading-with-glskeletonloader-in-app-a.yml
new file mode 100644
index 00000000000..9b2281c5af0
--- /dev/null
+++ b/changelogs/unreleased/244694-replace-gldeprecatedskeletonloading-with-glskeletonloader-in-app-a.yml
@@ -0,0 +1,5 @@
+---
+title: Replace deprecated skeleton loader in the user popover with slightly darker SVG based skelton loader
+merge_request: 59180
+author:
+type: other
diff --git a/changelogs/unreleased/323676-add-fk-to-partitioned-web-hook-logs.yml b/changelogs/unreleased/323676-add-fk-to-partitioned-web-hook-logs.yml
new file mode 100644
index 00000000000..5e3bf7f8372
--- /dev/null
+++ b/changelogs/unreleased/323676-add-fk-to-partitioned-web-hook-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Add a foreign key from the partitioned web_hook_logs to web_hooks
+merge_request: 59282
+author:
+type: other
diff --git a/changelogs/unreleased/323714-add-blob-filename-to-attachment-content-dispostion.yml b/changelogs/unreleased/323714-add-blob-filename-to-attachment-content-dispostion.yml
new file mode 100644
index 00000000000..84e98eff983
--- /dev/null
+++ b/changelogs/unreleased/323714-add-blob-filename-to-attachment-content-dispostion.yml
@@ -0,0 +1,5 @@
+---
+title: Add blob filename to attachment content disposition
+merge_request: 58977
+author:
+type: added
diff --git a/changelogs/unreleased/324786-update-deprecated-glicon-size-and-remove-use-deprecated-sizes.yml b/changelogs/unreleased/324786-update-deprecated-glicon-size-and-remove-use-deprecated-sizes.yml
new file mode 100644
index 00000000000..c32308ad8c7
--- /dev/null
+++ b/changelogs/unreleased/324786-update-deprecated-glicon-size-and-remove-use-deprecated-sizes.yml
@@ -0,0 +1,5 @@
+---
+title: Update Design Management added design icon to be slightly smaller which conforms to the Pajamas design guide
+merge_request: 58086
+author: Andreas Resch @reschandreas
+type: other
diff --git a/changelogs/unreleased/326197-project-runners-identifiers.yml b/changelogs/unreleased/326197-project-runners-identifiers.yml
new file mode 100644
index 00000000000..e9570fb9393
--- /dev/null
+++ b/changelogs/unreleased/326197-project-runners-identifiers.yml
@@ -0,0 +1,5 @@
+---
+title: Display project settings runners identifiers consistently
+merge_request: 59383
+author:
+type: changed
diff --git a/changelogs/unreleased/327106-reduce-timeout-on-search-count.yml b/changelogs/unreleased/327106-reduce-timeout-on-search-count.yml
new file mode 100644
index 00000000000..dd782105067
--- /dev/null
+++ b/changelogs/unreleased/327106-reduce-timeout-on-search-count.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce timeouts on tab counts for searches to 5s
+merge_request: 59435
+author:
+type: performance
diff --git a/changelogs/unreleased/327199-consistently-indicate-runner-type-using-the-alert-component.yml b/changelogs/unreleased/327199-consistently-indicate-runner-type-using-the-alert-component.yml
new file mode 100644
index 00000000000..12e6cb9fed9
--- /dev/null
+++ b/changelogs/unreleased/327199-consistently-indicate-runner-type-using-the-alert-component.yml
@@ -0,0 +1,5 @@
+---
+title: Update runner type indicators in view/edit pages
+merge_request: 59005
+author:
+type: changed
diff --git a/changelogs/unreleased/deprecate_alerts_managed_prometheus.yml b/changelogs/unreleased/deprecate_alerts_managed_prometheus.yml
new file mode 100644
index 00000000000..fab4cd8d5ae
--- /dev/null
+++ b/changelogs/unreleased/deprecate_alerts_managed_prometheus.yml
@@ -0,0 +1,5 @@
+---
+title: Deprecate Alerts for Managed Prometheus
+merge_request: 59433
+author:
+type: deprecated
diff --git a/changelogs/unreleased/drop-pipelines-async-when-user-blocked.yml b/changelogs/unreleased/drop-pipelines-async-when-user-blocked.yml
new file mode 100644
index 00000000000..d8e675baf6c
--- /dev/null
+++ b/changelogs/unreleased/drop-pipelines-async-when-user-blocked.yml
@@ -0,0 +1,5 @@
+---
+title: Drop user pipelines async when user is blocked
+merge_request: 59129
+author:
+type: fixed
diff --git a/changelogs/unreleased/exclude-projects-dropdown-from-revert.yml b/changelogs/unreleased/exclude-projects-dropdown-from-revert.yml
new file mode 100644
index 00000000000..76f1cb785e7
--- /dev/null
+++ b/changelogs/unreleased/exclude-projects-dropdown-from-revert.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude projects dropdown from revert modal
+merge_request: 59504
+author:
+type: fixed
diff --git a/changelogs/unreleased/id-enable-pick-into-project.yml b/changelogs/unreleased/id-enable-pick-into-project.yml
new file mode 100644
index 00000000000..9ed7c837dd5
--- /dev/null
+++ b/changelogs/unreleased/id-enable-pick-into-project.yml
@@ -0,0 +1,5 @@
+---
+title: Allow cherry-picking to a fork's parent
+merge_request: 59399
+author:
+type: added
diff --git a/changelogs/unreleased/jl-lower-milestone-issue-display-limit.yml b/changelogs/unreleased/jl-lower-milestone-issue-display-limit.yml
new file mode 100644
index 00000000000..9b0d23d6866
--- /dev/null
+++ b/changelogs/unreleased/jl-lower-milestone-issue-display-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce milestone issue list display limit to 500
+merge_request: 58168
+author:
+type: performance
diff --git a/changelogs/unreleased/mo-remove-codequality-backend-ff.yml b/changelogs/unreleased/mo-remove-codequality-backend-ff.yml
new file mode 100644
index 00000000000..eb28f6e8d73
--- /dev/null
+++ b/changelogs/unreleased/mo-remove-codequality-backend-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Remove codequality_backend_comparison feature flag
+merge_request: 59320
+author:
+type: added
diff --git a/changelogs/unreleased/optimize-environments-serializer.yml b/changelogs/unreleased/optimize-environments-serializer.yml
new file mode 100644
index 00000000000..89996a0a02e
--- /dev/null
+++ b/changelogs/unreleased/optimize-environments-serializer.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize environment serializer to reduce N+1 problems
+merge_request: 58748
+author:
+type: performance
diff --git a/changelogs/unreleased/re-enable-pages-serving-zip-from-disk.yml b/changelogs/unreleased/re-enable-pages-serving-zip-from-disk.yml
new file mode 100644
index 00000000000..c91f090ecc7
--- /dev/null
+++ b/changelogs/unreleased/re-enable-pages-serving-zip-from-disk.yml
@@ -0,0 +1,5 @@
+---
+title: Re-enable serving pages with zip file protocol
+merge_request: 59486
+author:
+type: added
diff --git a/config/feature_flags/development/attachment_with_filename.yml b/config/feature_flags/development/attachment_with_filename.yml
deleted file mode 100644
index 8d3a96404ef..00000000000
--- a/config/feature_flags/development/attachment_with_filename.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: attachment_with_filename
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55066
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323714
-milestone: '13.10'
-type: development
-group: group::editor
-default_enabled: false
diff --git a/config/feature_flags/development/codequality_backend_comparison.yml b/config/feature_flags/development/codequality_backend_comparison.yml
deleted file mode 100644
index 9383ecb97d7..00000000000
--- a/config/feature_flags/development/codequality_backend_comparison.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: codequality_backend_comparison
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53068
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300796
-milestone: '13.9'
-type: development
-group: group::testing
-default_enabled: true
diff --git a/config/feature_flags/development/pages_serve_with_zip_file_protocol.yml b/config/feature_flags/development/pages_serve_with_zip_file_protocol.yml
index 836702debba..153b0d5915c 100644
--- a/config/feature_flags/development/pages_serve_with_zip_file_protocol.yml
+++ b/config/feature_flags/development/pages_serve_with_zip_file_protocol.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321677
milestone: '13.6'
type: development
group: group::release
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/pick_into_project.yml b/config/feature_flags/development/pick_into_project.yml
index c180133b86f..fc28527d5a3 100644
--- a/config/feature_flags/development/pick_into_project.yml
+++ b/config/feature_flags/development/pick_into_project.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324154
milestone: '13.10'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/data/whats_new/202008180003_13_01.yml b/data/whats_new/202008180003_13_01.yml
index 9f8a87ebe27..835b0f8567b 100644
--- a/data/whats_new/202008180003_13_01.yml
+++ b/data/whats_new/202008180003_13_01.yml
@@ -12,7 +12,7 @@
image_url: https://about.gitlab.com/images/13_1/alert_management.png
published_at: 2020-06-22
release: 13.1
-- title: Accessibility Testing Merge Request Widget
+- title: Accessibility testing merge request widget
body: |
Today, developers who want to ensure their application is accessible to everyone suffer from slow feedback loops, which make it difficult to catch degradations in their code.
@@ -36,9 +36,9 @@
image_url: https://about.gitlab.com/images/13_1/resolve-design-comment.gif
published_at: 2020-06-22
release: 13.1
-- title: Merge Request Reviews moved to Free
+- title: Merge request reviews moved to Free
body: |
- Originally introduced in GitLab 11.4 as a GitLab Premium feature, Merge Request Reviews allow merge request reviewers to submit multiple comments at once, cutting down on notification noise for the merge request author, and allowing for a more cohesive and streamlined review process.
+ Originally introduced in GitLab 11.4 as a GitLab Premium feature, merge request reviews allow merge request reviewers to submit multiple comments at once, cutting down on notification noise for the merge request author, and allowing for a more cohesive and streamlined review process.
stage: Create
self-managed: true
gitlab-com: true
diff --git a/data/whats_new/202009150001_13_03.yml b/data/whats_new/202009150001_13_03.yml
index 3f51112d531..e9d63561af8 100644
--- a/data/whats_new/202009150001_13_03.yml
+++ b/data/whats_new/202009150001_13_03.yml
@@ -27,7 +27,7 @@
body: |
Dynamic Application Security Testing at GitLab has always been focused on integrating DAST into the DevOps pipeline and enabling developers to scan their review app, running website, or API for vulnerabilities as early as possible.
- However, there are times when it is necessary to run a DAST scan against an already deployed application when no code changes have been made and no Merge Request has been created. These scans could be needed for audit or compliance reasons, to debug and reproduce an issue that has been found, or to support teams who do not commit code, such as security analysts.
+ However, there are times when it is necessary to run a DAST scan against an already deployed application when no code changes have been made and no merge request has been created. These scans could be needed for audit or compliance reasons, to debug and reproduce an issue that has been found, or to support teams who do not commit code, such as security analysts.
Because of the need for DAST scans that are not triggered by a code change or MR, on-demand DAST testing is now available. You don’t need configuration files or code to start running on-demand scans. Configuration options for on-demand DAST scans are available within the GitLab UI.
stage: Secure
diff --git a/data/whats_new/202011230001_13_06.yml b/data/whats_new/202011230001_13_06.yml
index 47e8526408f..53ee394f156 100644
--- a/data/whats_new/202011230001_13_06.yml
+++ b/data/whats_new/202011230001_13_06.yml
@@ -16,9 +16,9 @@
release: 13.6
- title: Display Code Quality severity ratings
body: |
- The Code Quality feature in GitLab is great at showing what quality violations exist in a project or are changing in the Merge Request. However, understanding which of those violations is the most important is not clear in the GitLab interface today.
+ The Code Quality feature in GitLab is great at showing what quality violations exist in a project or are changing in the merge request. However, understanding which of those violations is the most important is not clear in the GitLab interface today.
- With the Full Code Quality Report and Merge Request Widget, now you can see the severity rating. This makes it easy for you to understand which code quality violations are most important to resolve before merging and reduces the technical debt in your project.
+ With the Full Code Quality Report and merge request widget, now you can see the severity rating. This makes it easy for you to understand which code quality violations are most important to resolve before merging and reduces the technical debt in your project.
stage: Verify
self-managed: true
gitlab-com: true
diff --git a/data/whats_new/202012160001_13_07.yml b/data/whats_new/202012160001_13_07.yml
index 702c757a280..28d596081cc 100644
--- a/data/whats_new/202012160001_13_07.yml
+++ b/data/whats_new/202012160001_13_07.yml
@@ -10,7 +10,7 @@
image_url: https://img.youtube.com/vi/G8fYYrxqF5E/hqdefault.jpg
published_at: 2020-12-22
release: 13.7
-- title: Reviewers for Merge Requests
+- title: Reviewers for merge requests
body: |
Asking a colleague to review your code should be a routine part of contributing code, but it's often needlessly complex. A simple task like asking for a review can lead to confusion. For example, how should you ask? An email? Comment? Chat message? Without a formal process, reviews can be inconsistent and hard to keep track of. Previously, an option was to assign a reviewer to a merge request, but even with this formality, both the author and the reviewer appeared in the same assignee field, making it hard for other team members to know who was doing what.
diff --git a/data/whats_new/202103220001_13_10.yml b/data/whats_new/202103220001_13_10.yml
index 6de61ecdb06..24928f9609b 100644
--- a/data/whats_new/202103220001_13_10.yml
+++ b/data/whats_new/202103220001_13_10.yml
@@ -74,13 +74,13 @@
image_url: https://about.gitlab.com/images/13_10/integrate_alerts.png
published_at: 2021-03-22
release: 13.10
-- title: "Merge Request test summary usability improvements"
+- title: "Merge request test summary usability improvements"
body: |
- Increasing the number of tests or custom metrics in a pipeline gives you additional confidence and information. However, increasing these to a large number has also come with a degraded visual experience of the Merge Request page. The Merge Request test summary widget has been improved so you can better differentiate between the different test jobs in the widget, making it easier to identify which job contains failed tests.
+ Increasing the number of tests or custom metrics in a pipeline gives you additional confidence and information. However, increasing these to a large number has also come with a degraded visual experience of the merge request page. The merge request test summary widget has been improved so you can better differentiate between the different test jobs in the widget, making it easier to identify which job contains failed tests.
It has also been challenging to understand why a `junit.xml` file was not parsed without errors being presented. Now you can see parsing errors in the Test Summary widget, as well as the Unit Test report, to identify and resolve structural issues and see test results in GitLab.
- The [Metrics Reports](https://docs.gitlab.com/ee/ci/metrics_reports.html) widget [(Premium and Ultimate)](https://about.gitlab.com/pricing/) is now sorted so new, changed, and unchanged metrics are all together, making the experience of finding metrics that have changed as part of the Merge Request more intuitive.
+ The [Metrics Reports](https://docs.gitlab.com/ee/ci/metrics_reports.html) widget [(Premium and Ultimate)](https://about.gitlab.com/pricing/) is now sorted so new, changed, and unchanged metrics are all together, making the experience of finding metrics that have changed as part of the merge request more intuitive.
stage: Verify
self-managed: true
gitlab-com: true
diff --git a/data/whats_new/templates/YYYYMMDD0001_XX_YY.yml b/data/whats_new/templates/YYYYMMDD0001_XX_YY.yml
index 74915c19f95..f8611da2cff 100644
--- a/data/whats_new/templates/YYYYMMDD0001_XX_YY.yml
+++ b/data/whats_new/templates/YYYYMMDD0001_XX_YY.yml
@@ -7,7 +7,7 @@
# For more information please refer to the handbook documentation here:
# https://about.gitlab.com/handbook/marketing/blog/release-posts/index.html#create-mr-for-whats-new-entries
#
-# Please delete this line and above before submitting your Merge Request.
+# Please delete this line and above before submitting your merge request.
- title: # Match the release post entry
body: | # Do not modify this line, instead modify the lines below.
diff --git a/db/post_migrate/20210413130011_add_partitioned_web_hook_log_fk.rb b/db/post_migrate/20210413130011_add_partitioned_web_hook_log_fk.rb
new file mode 100644
index 00000000000..6453993bd51
--- /dev/null
+++ b/db/post_migrate/20210413130011_add_partitioned_web_hook_log_fk.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddPartitionedWebHookLogFk < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::PartitioningMigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_partitioned_foreign_key :web_hook_logs_part_0c5294f417,
+ :web_hooks,
+ column: :web_hook_id,
+ on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :web_hook_logs_part_0c5294f417, column: :web_hook_id
+ end
+ end
+end
diff --git a/db/schema_migrations/20210413130011 b/db/schema_migrations/20210413130011
new file mode 100644
index 00000000000..c67e9705e7a
--- /dev/null
+++ b/db/schema_migrations/20210413130011
@@ -0,0 +1 @@
+943466b272406a95c478337de84f72388dae88a8cf88f3b389e3ade9d4ecd63d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 82dc0f21b2c..6df80a14d0f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -26480,6 +26480,9 @@ ALTER TABLE ONLY approval_project_rules_users
ALTER TABLE ONLY lists
ADD CONSTRAINT fk_rails_baed5f39b7 FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE CASCADE;
+ALTER TABLE web_hook_logs_part_0c5294f417
+ ADD CONSTRAINT fk_rails_bb3355782d FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY security_findings
ADD CONSTRAINT fk_rails_bb63863cf1 FOREIGN KEY (scan_id) REFERENCES security_scans(id) ON DELETE CASCADE;
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 06475f17822..820006eeadf 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -145,13 +145,14 @@ limited to 1KiB, and descriptions (the rest of the message) will be limited to
## Number of issues in the milestone overview
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/39453) in GitLab 12.10.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/39453) in GitLab 12.10.
+> - [Set](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58168) to 500 in GitLab 13.11.
-The maximum number of issues loaded on the milestone overview page is 3000.
+The maximum number of issues loaded on the milestone overview page is 500.
When the number exceeds the limit the page displays an alert and links to a paginated
[issue list](../user/project/issues/managing_issues.md) of all issues in the milestone.
-- **Limit:** 3000 issues
+- **Limit:** 500 issues
## Number of pipelines per Git push
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index c803c888449..e6281f511fd 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1960,7 +1960,7 @@ Represents a DAST Profile.
| Field | Type | Description |
| ----- | ---- | ----------- |
-| `branch` | [`DastProfileBranch`](#dastprofilebranch) | The associated branch. Will always return `null` if `dast_branch_selection` feature flag is disabled. |
+| `branch` | [`DastProfileBranch`](#dastprofilebranch) | The associated branch. |
| `dastScannerProfile` | [`DastScannerProfile`](#dastscannerprofile) | The associated scanner profile. |
| `dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | The associated site profile. |
| `description` | [`String`](#string) | The description of the scan. |
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 0bf520b69c1..78af6b881aa 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -459,6 +459,86 @@ Example of response
}
```
+## Get Kubernetes Agents by `CI_JOB_TOKEN` **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324269) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.11.
+
+Retrieve the job that generated the `CI_JOB_TOKEN`, along with a list of allowed GitLab
+Kubernetes Agents.
+
+```plaintext
+GET /job/allowed_agents
+```
+
+Supported attributes:
+
+| Attribute | Type | Required | Description |
+|:------------ |:---------|:---------|:----------------------|
+| `CI_JOB_TOKEN` | string | yes | Token value associated with the GitLab-provided `CI_JOB_TOKEN` variable. |
+
+Example request:
+
+```shell
+curl --header "JOB-TOKEN: <CI_JOB_TOKEN>" "https://gitlab.example.com/api/v4/job/allowed_agents"
+curl "https://gitlab.example.com/api/v4/job/allowed_agents?job_token=<CI_JOB_TOKEN>"
+```
+
+Example response:
+
+```json
+{
+ "allowed_agents":
+ [
+ {
+ "id": 1,
+ "config_project": {
+ "id": 1,
+ "description": null,
+ "name": "project1",
+ "name_with_namespace": "John Doe2 / project1",
+ "path": "project1",
+ "path_with_namespace": "namespace1/project1",
+ "created_at": "2021-03-26T14:51:50.579Z"
+ }
+ }
+ ],
+ "job": {
+ "id": 1,
+ "name": "test",
+ "stage": "test",
+ "project_id": 1,
+ "project_name": "project1"
+ },
+ "pipeline": {
+ "id": 1,
+ "project_id": 1,
+ "sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "ref": "master",
+ "status": "pending",
+ "created_at": "2021-03-26T14:51:51.107Z",
+ "updated_at": "2021-03-26T14:51:51.107Z",
+ "web_url": "http://localhost/namespace1/project1/-/pipelines/1"
+ },
+ "project": {
+ "id": 1,
+ "description": null,
+ "name": "project1",
+ "name_with_namespace": "John Doe2 / project1",
+ "path": "project1",
+ "path_with_namespace": "namespace1/project1",
+ "created_at": "2021-03-26T14:51:50.579Z"
+ },
+ "user": {
+ "id": 2,
+ "name": "John Doe3",
+ "username": "user2",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/10fc7f102b",
+ "web_url": "http://localhost/user2"
+ }
+}
+```
+
## Get a single job
Get a single job of a project
diff --git a/doc/ci/services/gitlab.md b/doc/ci/services/gitlab.md
new file mode 100644
index 00000000000..d9962d9d1f2
--- /dev/null
+++ b/doc/ci/services/gitlab.md
@@ -0,0 +1,44 @@
+---
+stage: Verify
+group: Runner
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+type: reference
+---
+
+# Using GitLab
+
+As many applications depend on accessing JSON apis you eventually need them in order for your tests to run.
+In this example we are providing GitLab as a Microservice to be accessible for API clients.
+Below you are guided how to do this with the Docker executors of GitLab Runner.
+
+## Use GitLab with the Docker executor
+
+If you're using [GitLab Runner](../runners/README.md) with the Docker/Kubernetes executor,
+you basically have everything set up already.
+
+First, in your `.gitlab-ci.yml` add:
+
+```yaml
+services:
+ - name: gitlab/gitlab-ce:latest
+ alias: gitlab
+
+variables:
+ GITLAB_HTTPS: "false" # ensure that plain http will work
+ GITLAB_ROOT_PASSWORD: "password" # in order to access the api with user root:password
+```
+
+To set values for the `GITLAB_HTTPS`, `GITLAB_ROOT_PASSWORD`,
+[assign them to a variable in the user interface](../variables/README.md#project-cicd-variables),
+then assign that variable to the corresponding variable in your
+`.gitlab-ci.yml` file.
+
+From your ci `script:` the API will then be availible at `http://gitlab/api/v4`
+
+If you're wondering why we used `gitlab` for the `Host`, read more at
+[How services are linked to the job](../docker/using_docker_images.md#extended-docker-configuration-options).
+
+You can also use any other Docker image available on [Docker Hub](https://hub.docker.com/u/gitlab).
+
+The `gitlab` image can accept some environment variables. For more details,
+see the [omnibus documentation](../../install/README.md).
diff --git a/doc/ci/services/index.md b/doc/ci/services/index.md
index 6754e547d0d..8d603b17e2e 100644
--- a/doc/ci/services/index.md
+++ b/doc/ci/services/index.md
@@ -18,6 +18,7 @@ case is to run a database container, for example:
- [MySQL](mysql.md)
- [PostgreSQL](postgres.md)
- [Redis](redis.md)
+- [GitLab](gitlab.md) as an example for a microservice offering a JSON API
It's easier and faster to use an existing image and run it as an additional container
than to install `mysql`, for example, every time the project is built.
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 8936f8c05ee..ee80d998c14 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -48,7 +48,6 @@ the `author` field. GitLab team members **should not**.
- Any client-facing change to our REST and GraphQL APIs **must** have a changelog entry. See the [complete list what comprises a GraphQL breaking change](api_graphql_styleguide.md#breaking-changes).
- Any change that introduces an [Advanced Search migration](elasticsearch.md#creating-a-new-advanced-search-migration) **must** have a changelog entry.
- Performance improvements **should** have a changelog entry.
-- Changes that need to be documented in the Product Intelligence [Event Dictionary](https://about.gitlab.com/handbook/product/product-intelligence-guide/#event-dictionary)
also require a changelog entry.
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
@@ -56,7 +55,7 @@ the `author` field. GitLab team members **should not**.
- Any docs-only changes **should not** have a changelog entry.
- Any change behind a feature flag **disabled** by default **should not** have a changelog entry.
- Any change behind a feature flag that is **enabled** by default **should** have a changelog entry.
-- Any change that adds new usage data metrics and changes that needs to be documented in Product Intelligence [Event Dictionary](https://about.gitlab.com/handbook/product/product-intelligence-guide/#event-dictionary) **should** have a changelog entry.
+- Any change that adds new Usage Data metrics and changes that needs to be documented in Product Intelligence [Metrics Dictionary](usage_ping/dictionary.md) **should** have a changelog entry.
- A change that adds snowplow events **should** have a changelog entry -
- A change that [removes a feature flag, or removes a feature and its feature flag](feature_flags/index.md) **must** have a changelog entry.
- A fix for a regression introduced and then fixed in the same release (i.e.,
diff --git a/doc/development/deprecation_guidelines/index.md b/doc/development/deprecation_guidelines/index.md
index c2fea3c6053..2d092b24d65 100644
--- a/doc/development/deprecation_guidelines/index.md
+++ b/doc/development/deprecation_guidelines/index.md
@@ -26,4 +26,8 @@ A feature can be deprecated at any time, provided there is a viable alternative.
## When can a feature be removed/changed?
-See our [Release and Maintenance policy](../../policy/maintenance.md).
+For API removals, see the [GraphQL](../../api/graphql/index.md#deprecation-and-removal-process) and [GitLab API](../../api/README.md#compatibility-guidelines) guidelines.
+
+For configuration removals, see the [Omnibus deprecation policy](https://docs.gitlab.com/omnibus/package-information/deprecation_policy.html).
+
+For versioning and upgrade details, see our [Release and Maintenance policy](../../policy/maintenance.md).
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index f47c81f3f13..cb92394c083 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -14640,7 +14640,7 @@ Tiers: `free`
### `usage_activity_by_stage.create.merge_requests_with_added_rules`
-Merge Requests with added rules
+Merge requests with added rules
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216175047_merge_requests_with_added_rules.yml)
@@ -16584,7 +16584,7 @@ Tiers: `free`
### `usage_activity_by_stage_monthly.create.merge_requests_with_added_rules`
-Merge Requests with added rules
+Merge requests with added rules
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210216175103_merge_requests_with_added_rules.yml)
diff --git a/doc/operations/metrics/alerts.md b/doc/operations/metrics/alerts.md
index 472892c77f9..7763224d21e 100644
--- a/doc/operations/metrics/alerts.md
+++ b/doc/operations/metrics/alerts.md
@@ -17,6 +17,10 @@ your team when environment performance falls outside of the boundaries you set.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2 for [custom metrics](index.md#adding-custom-metrics), and GitLab 11.3 for [library metrics](../../user/project/integrations/prometheus_library/index.md).
+WARNING:
+Managed Prometheus on Kubernetes is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/327796)
+and scheduled for [removal in GitLab 14.0](https://gitlab.com/groups/gitlab-org/-/epics/4280).
+
For managed Prometheus instances using auto configuration, you can
[configure alerts for metrics](index.md#adding-custom-metrics) directly in the
[metrics dashboard](index.md). To set an alert:
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index a3d7459e216..5b32157d28e 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -820,6 +820,7 @@ Alternatively, you can use the CI/CD variable `SECURE_ANALYZERS_PREFIX` to overr
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/218465) in GitLab 13.3.
> - The saved scans feature was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5100) in GitLab 13.9.
> - The option to select a branch was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/4847) in GitLab 13.10.
+> - DAST branch selection [feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/322672) in GitLab 13.11.
An on-demand DAST scan runs outside the DevOps life cycle. Changes in your repository don't trigger
the scan. You must start it manually.
@@ -831,10 +832,7 @@ An on-demand DAST scan:
- Is associated with your project's default branch.
- Is saved on creation so it can be run later.
-In GitLab 13.10 and later, you can select to run an on-demand scan against a specific branch. This
-feature is [deployed behind a feature flag](../../feature_flags.md), enabled by default. It's
-enabled on GitLab.com and recommended for production use. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
-can opt to disable it with `Feature.disable(:dast_branch_selection)`.
+In GitLab 13.10 and later, you can select to run an on-demand scan against a specific branch.
### On-demand scan modes
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index d3b5a51c756..45e9d5bfd20 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -475,6 +475,12 @@ You should consider these security implications before configuring IP address re
- **Administrators and group owners**: Users with these permission levels can always
access the group settings, regardless of IP restriction, but they cannot access projects
belonging to the group when accessing from a disallowed IP address.
+- **GitLab API and runner activities**: Only the [Groups](../../api/groups.md)
+ and [Projects](../../api/projects.md) APIs are protected by IP address restrictions.
+ When you register a runner, it is not bound by the IP restrictions. When the runner
+ requests a new job or an update to a job's state, it is also not bound by
+ the IP restrictions. But when the running CI/CD job sends Git requests from a
+ restricted IP address, the IP restriction prevents code from being cloned.
To restrict group access by IP address:
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index ad2d2ac2a8e..3dd900d2cbe 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -68,6 +68,11 @@ The requirement to authenticate is a breaking change added in 13.7. An [administ
disable it](../../../administration/packages/dependency_proxy.md#disabling-authentication) if it
has disrupted your existing Dependency Proxy usage.
+WARNING:
+If [SSO enforcement](../../group/saml_sso/index.md#sso-enforcement)
+is enabled for your Group, requests to the dependency proxy will fail. This bug is being tracked in
+[this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/294018).
+
Because the Dependency Proxy is storing Docker images in a space associated with your group,
you must authenticate against the Dependency Proxy.
diff --git a/doc/user/project/issues/sorting_issue_lists.md b/doc/user/project/issues/sorting_issue_lists.md
index 3a393b18579..97a790c2527 100644
--- a/doc/user/project/issues/sorting_issue_lists.md
+++ b/doc/user/project/issues/sorting_issue_lists.md
@@ -24,6 +24,12 @@ For sorting by issue priority, see [Label Priority](../labels.md#label-priority)
In group and project issue lists, it is also possible to order issues manually,
similar to [issue boards](../issue_board.md#how-gitlab-orders-issues-in-a-list).
+## Sorting by popularity
+
+When you select sorting by **Popularity**, the issue order changes to sort descending by the
+number of upvotes ([awarded](../../award_emojis.md) "thumbs up" emoji)
+on each issue. You can use this to identify issues that are in high demand.
+
## Manual sorting
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/62178) in GitLab 12.2.
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index b469c9d4a57..b4b9e91dd94 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -571,3 +571,8 @@ plugins:
enabled: true
channel: rubocop-0-67
```
+
+### No Code Quality appears on merge requests when using custom tool
+
+If your merge requests do not show any code quality changes when using a custom tool,
+ensure that the line property is an `integer`.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 284deabb33c..e1815785fb5 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -68,7 +68,7 @@ threads. Some quick actions might not be available to all subscription tiers.
| `/due <date>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
| `/duplicate <#issue>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Close this issue and mark as a duplicate of another issue. **(FREE)** Also, mark both as related. |
| `/epic <epic>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. |
-| `/estimate <<W>w <DD>d <hh>h <mm>m>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Set time estimate. For example, `/estimate 1w 3d 2h 14m`. |
+| `/estimate <time>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Set time estimate. For example, `/estimate 1mo 2w 3d 4h 5m`. Learn more about [time tracking](time_tracking.md). |
| `/invite_email email1 email2` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add up to six email participants. This action is behind feature flag `issue_email_participants`. |
| `/iteration *iteration:"iteration name"` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set iteration. For example, to set the `Late in July` iteration: `/iteration *iteration:"Late in July"` ([introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)). |
| `/label ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Add one or more labels. Label names can also start without a tilde (`~`), but mixed syntax is not supported. |
@@ -95,8 +95,7 @@ threads. Some quick actions might not be available to all subscription tiers.
| `/remove_zoom` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Remove Zoom meeting from this issue ([introduced in GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609)). |
| `/reopen` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Reopen. |
| `/shrug <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `¯\_(ツ)_/¯`. |
-| `/spend <time(-<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Subtract spent time. Optionally, specify the date that time was spent on. For example, `/spend time(-1h 30m)` or `/spend time(-1h 30m) date(2018-08-26)`. |
-| `/spend <time(<h>h <mm>m)> <date(<YYYY-MM-DD>)>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Add spent time. Optionally, specify the date that time was spent on. For example, `/spend time(1h 30m)` or `/spend time(1h 30m) date(2018-08-26)`. |
+| `/spend <time> [<date>]` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Add or subtract spent time. Optionally, specify the date that time was spent on. For example, `/spend 1mo 2w 3d 4h 5m 2018-08-26` or `/spend -1h 30m`. Learn more about [time tracking](time_tracking.md). |
| `/submit_review` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Submit a pending review ([introduced in GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/issues/8041)). |
| `/subscribe` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Subscribe to notifications. |
| `/tableflip <comment>` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Append the comment with `(╯°□°)╯︵ ┻━┻`. |
diff --git a/doc/user/project/time_tracking.md b/doc/user/project/time_tracking.md
index ad7642d8d8a..78e7ded9784 100644
--- a/doc/user/project/time_tracking.md
+++ b/doc/user/project/time_tracking.md
@@ -44,9 +44,9 @@ with [Reporter and higher permission levels](../permissions.md).
### Estimates
To enter an estimate, write `/estimate`, followed by the time. For example, if
-you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write
-`/estimate 3d 5h 10m`. Time units that we support are listed at the bottom of
-this help page.
+you need to enter an estimate of 1 month, 2 weeks, 3 days, 4 hours and 5 minutes,
+write `/estimate 1mo 2w 3d 4h 5m`.
+Check the [time units you can use](#configuration).
Every time you enter a new time estimate, any previous time estimates are
overridden by this new value. There should only be one valid estimate in an
@@ -56,7 +56,9 @@ To remove an estimation entirely, use `/remove_estimate`.
### Time spent
-To enter a time spent, use `/spend 3d 5h 10m`.
+To enter time spent, write `/spend`, followed by the time. For example, if you need
+to log 1 month, 2 weeks, 3 days, 4 hours and 5 minutes, you would write `/spend 1mo 2w 3d 4h 5m`.
+Time units that we support are listed at the bottom of this help page.
Every new time spent entry is added to the current total time spent for the
issue or the merge request.
@@ -66,6 +68,11 @@ days from the total time spent. You can't go below 0 minutes of time spent,
so GitLab automatically resets the time spent if you remove a larger amount
of time compared to the time that was entered already.
+You can log time in the past by providing a date after the time.
+For example, if you want to log 1 hour of time spent on the 31 January 2021,
+you would write `/spend 1h 2021-01-31`. If you supply a date in the future, the
+command fails and no time is logged.
+
To remove all the time spent at once, use `/remove_time_spent`.
## Configuration
diff --git a/lib/api/entities/clusters/agent.rb b/lib/api/entities/clusters/agent.rb
new file mode 100644
index 00000000000..3b4538b81c2
--- /dev/null
+++ b/lib/api/entities/clusters/agent.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Clusters
+ class Agent < Grape::Entity
+ expose :id
+ expose :project, with: Entities::ProjectIdentity, as: :config_project
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/job_info.rb b/lib/api/entities/job_request/job_info.rb
index 09c13aa8471..a4bcc9726d0 100644
--- a/lib/api/entities/job_request/job_info.rb
+++ b/lib/api/entities/job_request/job_info.rb
@@ -4,7 +4,7 @@ module API
module Entities
module JobRequest
class JobInfo < Grape::Entity
- expose :name, :stage
+ expose :id, :name, :stage
expose :project_id, :project_name
end
end
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 7390219b60e..54951f9bd01 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -6,8 +6,6 @@ module API
before { authenticate! }
- feature_category :continuous_integration
-
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :id, type: String, desc: 'The ID of a project'
@@ -40,7 +38,7 @@ module API
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
- get ':id/jobs' do
+ get ':id/jobs', feature_category: :continuous_integration do
authorize_read_builds!
builds = user_project.builds.order('id DESC')
@@ -57,7 +55,7 @@ module API
params do
requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/jobs/:job_id' do
+ get ':id/jobs/:job_id', feature_category: :continuous_integration do
authorize_read_builds!
build = find_build!(params[:job_id])
@@ -72,7 +70,7 @@ module API
params do
requires :job_id, type: Integer, desc: 'The ID of a job'
end
- get ':id/jobs/:job_id/trace' do
+ get ':id/jobs/:job_id/trace', feature_category: :continuous_integration do
authorize_read_builds!
build = find_build!(params[:job_id])
@@ -94,7 +92,7 @@ module API
params do
requires :job_id, type: Integer, desc: 'The ID of a job'
end
- post ':id/jobs/:job_id/cancel' do
+ post ':id/jobs/:job_id/cancel', feature_category: :continuous_integration do
authorize_update_builds!
build = find_build!(params[:job_id])
@@ -111,7 +109,7 @@ module API
params do
requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/jobs/:job_id/retry' do
+ post ':id/jobs/:job_id/retry', feature_category: :continuous_integration do
authorize_update_builds!
build = find_build!(params[:job_id])
@@ -129,7 +127,7 @@ module API
params do
requires :job_id, type: Integer, desc: 'The ID of a build'
end
- post ':id/jobs/:job_id/erase' do
+ post ':id/jobs/:job_id/erase', feature_category: :continuous_integration do
authorize_update_builds!
build = find_build!(params[:job_id])
@@ -148,7 +146,7 @@ module API
requires :job_id, type: Integer, desc: 'The ID of a Job'
end
- post ":id/jobs/:job_id/play" do
+ post ":id/jobs/:job_id/play", feature_category: :continuous_integration do
authorize_read_builds!
job = find_job!(params[:job_id])
@@ -174,10 +172,8 @@ module API
success Entities::Ci::Job
end
route_setting :authentication, job_token_allowed: true
- get do
- # current_authenticated_job will be nil if user is using
- # a valid authentication that is not CI_JOB_TOKEN
- not_found!('Job') unless current_authenticated_job
+ get '', feature_category: :continuous_integration do
+ validate_current_authenticated_job
present current_authenticated_job, with: Entities::Ci::Job
end
@@ -196,6 +192,14 @@ module API
builds.where(status: available_statuses && scope)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def validate_current_authenticated_job
+ # current_authenticated_job will be nil if user is using
+ # a valid authentication (like PRIVATE-TOKEN) that is not CI_JOB_TOKEN
+ not_found!('Job') unless current_authenticated_job
+ end
end
end
end
+
+API::Jobs.prepend_if_ee('EE::API::Jobs')
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
deleted file mode 100644
index 72a4230cfe1..00000000000
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ /dev/null
@@ -1,446 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # Issues, merge requests, Snippets, Commits and Commit Ranges share
- # similar functionality in reference filtering.
- class AbstractReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
- # reference (which we replace with placeholder during re-scaping). The
- # random number helps ensure it's pretty close to unique. Since it's a
- # transitory value (it never gets saved) we can initialize once, and it
- # doesn't matter if it changes on a restart.
- REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
- REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
-
- def self.object_class
- # Implement in child class
- # Example: MergeRequest
- end
-
- def self.object_name
- @object_name ||= object_class.name.underscore
- end
-
- def self.object_sym
- @object_sym ||= object_name.to_sym
- end
-
- # Public: Find references in text (like `!123` for merge requests)
- #
- # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
- # object = find_object(project_ref, id)
- # "<a href=...>#{object.to_reference}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer referenced object ID, an optional String
- # of the external project reference, and all of the matchdata.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text, pattern = object_class.reference_pattern)
- text.gsub(pattern) do |match|
- if ident = identifier($~)
- yield match, ident, $~[:project], $~[:namespace], $~
- else
- match
- end
- end
- end
-
- def self.identifier(match_data)
- symbol = symbol_from_match(match_data)
-
- parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
- end
-
- def identifier(match_data)
- self.class.identifier(match_data)
- end
-
- def self.symbol_from_match(match)
- key = object_sym
- match[key] if match.names.include?(key.to_s)
- end
-
- # Transform a symbol extracted from the text to a meaningful value
- # In most cases these will be integers, so we call #to_i by default
- #
- # This method has the contract that if a string `ref` refers to a
- # record `record`, then `parse_symbol(ref) == record_identifier(record)`.
- def self.parse_symbol(symbol, match_data)
- symbol.to_i
- end
-
- # We assume that most classes are identifying records by ID.
- #
- # This method has the contract that if a string `ref` refers to a
- # record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
- def record_identifier(record)
- record.id
- end
-
- def object_class
- self.class.object_class
- end
-
- def object_sym
- self.class.object_sym
- end
-
- def references_in(*args, &block)
- self.class.references_in(*args, &block)
- end
-
- # Implement in child class
- # Example: project.merge_requests.find
- def find_object(parent_object, id)
- end
-
- # Override if the link reference pattern produces a different ID (global
- # ID vs internal ID, for instance) to the regular reference pattern.
- def find_object_from_link(parent_object, id)
- find_object(parent_object, id)
- end
-
- # Implement in child class
- # Example: project_merge_request_url
- def url_for_object(object, parent_object)
- end
-
- def find_object_cached(parent_object, id)
- cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do
- find_object(parent_object, id)
- end
- end
-
- def find_object_from_link_cached(parent_object, id)
- cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do
- find_object_from_link(parent_object, id)
- end
- end
-
- def from_ref_cached(ref)
- cached_call("banzai_#{parent_type}_refs".to_sym, ref) do
- parent_from_ref(ref)
- end
- end
-
- def url_for_object_cached(object, parent_object)
- cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do
- url_for_object(object, parent_object)
- end
- end
-
- def call
- return doc unless project || group || user
-
- ref_pattern = object_class.reference_pattern
- link_pattern = object_class.link_reference_pattern
-
- # Compile often used regexps only once outside of the loop
- ref_pattern_anchor = /\A#{ref_pattern}\z/
- link_pattern_start = /\A#{link_pattern}/
- link_pattern_anchor = /\A#{link_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node) && ref_pattern
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- object_link_filter(content, ref_pattern)
- end
-
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if ref_pattern && link =~ ref_pattern_anchor
- replace_link_node_with_href(node, index, link) do
- object_link_filter(link, ref_pattern, link_content: inner_html)
- end
-
- next
- end
-
- next unless link_pattern
-
- if link == inner_html && inner_html =~ link_pattern_start
- replace_link_node_with_text(node, index) do
- object_link_filter(inner_html, link_pattern, link_reference: true)
- end
-
- next
- end
-
- if link =~ link_pattern_anchor
- replace_link_node_with_href(node, index, link) do
- object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
- end
-
- next
- end
- end
- end
- end
-
- doc
- end
-
- # Replace references (like `!123` for merge requests) in text with links
- # to the referenced object's details page.
- #
- # text - String text to replace references in.
- # pattern - Reference pattern to match against.
- # link_content - Original content of the link being replaced.
- # link_reference - True if this was using the link reference pattern,
- # false otherwise.
- #
- # Returns a String with references replaced with links. All links
- # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
- def object_link_filter(text, pattern, link_content: nil, link_reference: false)
- references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
- parent_path = if parent_type == :group
- full_group_path(namespace_ref)
- else
- full_project_path(namespace_ref, project_ref)
- end
-
- parent = from_ref_cached(parent_path)
-
- if parent
- object =
- if link_reference
- find_object_from_link_cached(parent, id)
- else
- find_object_cached(parent, id)
- end
- end
-
- if object
- title = object_link_title(object, matches)
- klass = reference_class(object_sym)
-
- data_attributes = data_attributes_for(link_content || match, parent, object,
- link_content: !!link_content,
- link_reference: link_reference)
- data = data_attribute(data_attributes)
-
- url =
- if matches.names.include?("url") && matches[:url]
- matches[:url]
- else
- url_for_object_cached(object, parent)
- end
-
- content = link_content || object_link_text(object, matches)
-
- link = %(<a href="#{url}" #{data}
- title="#{escape_once(title)}"
- class="#{klass}">#{content}</a>)
-
- wrap_link(link, object)
- else
- match
- end
- end
- end
-
- def wrap_link(link, object)
- link
- end
-
- def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
- object_parent_type = parent.is_a?(Group) ? :group : :project
-
- {
- original: escape_html_entities(text),
- link: link_content,
- link_reference: link_reference,
- object_parent_type => parent.id,
- object_sym => object.id
- }
- end
-
- def object_link_text_extras(object, matches)
- extras = []
-
- if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
- extras << "comment #{Regexp.last_match(1)}"
- end
-
- extension = matches[:extension] if matches.names.include?("extension")
-
- extras << extension if extension
-
- extras
- end
-
- def object_link_title(object, matches)
- object.title
- end
-
- def object_link_text(object, matches)
- parent = project || group || user
- text = object.reference_link_text(parent)
-
- extras = object_link_text_extras(object, matches)
- text += " (#{extras.join(", ")})" if extras.any?
-
- text
- end
-
- # Returns a Hash containing all object references (e.g. issue IDs) per the
- # project they belong to.
- def references_per_parent
- @references_per ||= {}
-
- @references_per[parent_type] ||= begin
- refs = Hash.new { |hash, key| hash[key] = Set.new }
- regex = [
- object_class.link_reference_pattern,
- object_class.reference_pattern
- ].compact.reduce { |a, b| Regexp.union(a, b) }
-
- nodes.each do |node|
- node.to_html.scan(regex) do
- path = if parent_type == :project
- full_project_path($~[:namespace], $~[:project])
- else
- full_group_path($~[:group])
- end
-
- if ident = identifier($~)
- refs[path] << ident
- end
- end
- end
-
- refs
- end
- end
-
- # Returns a Hash containing referenced projects grouped per their full
- # path.
- def parent_per_reference
- @per_reference ||= {}
-
- @per_reference[parent_type] ||= begin
- refs = Set.new
-
- references_per_parent.each do |ref, _|
- refs << ref
- end
-
- find_for_paths(refs.to_a).index_by(&:full_path)
- end
- end
-
- def relation_for_paths(paths)
- klass = parent_type.to_s.camelize.constantize
- result = klass.where_full_path_in(paths)
- return result if parent_type == :group
-
- result.includes(:namespace) if parent_type == :project
- end
-
- # Returns projects for the given paths.
- def find_for_paths(paths)
- if Gitlab::SafeRequestStore.active?
- cache = refs_cache
- to_query = paths - cache.keys
-
- unless to_query.empty?
- records = relation_for_paths(to_query)
-
- found = []
- records.each do |record|
- ref = record.full_path
- get_or_set_cache(cache, ref) { record }
- found << ref
- end
-
- not_found = to_query - found
- not_found.each do |ref|
- get_or_set_cache(cache, ref) { nil }
- end
- end
-
- cache.slice(*paths).values.compact
- else
- relation_for_paths(paths)
- end
- end
-
- def current_parent_path
- @current_parent_path ||= parent&.full_path
- end
-
- def current_project_namespace_path
- @current_project_namespace_path ||= project&.namespace&.full_path
- end
-
- def records_per_parent
- @_records_per_project ||= {}
-
- @_records_per_project[object_class.to_s.underscore] ||= begin
- hash = Hash.new { |h, k| h[k] = {} }
-
- parent_per_reference.each do |path, parent|
- record_ids = references_per_parent[path]
-
- parent_records(parent, record_ids).each do |record|
- hash[parent][record_identifier(record)] = record
- end
- end
-
- hash
- end
- end
-
- private
-
- def full_project_path(namespace, project_ref)
- return current_parent_path unless project_ref
-
- namespace_ref = namespace || current_project_namespace_path
- "#{namespace_ref}/#{project_ref}"
- end
-
- def refs_cache
- Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
- end
-
- def parent_type
- :project
- end
-
- def parent
- parent_type == :project ? project : group
- end
-
- def full_group_path(group_ref)
- return current_parent_path unless group_ref
-
- group_ref
- end
-
- def unescape_html_entities(text)
- CGI.unescapeHTML(text.to_s)
- end
-
- def escape_html_entities(text)
- CGI.escapeHTML(text.to_s)
- end
-
- def escape_with_placeholders(text, placeholder_data)
- escaped = escape_html_entities(text)
-
- escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
- placeholder_data[Regexp.last_match(1).to_i]
- end
- end
- end
- end
-end
-
-Banzai::Filter::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::AbstractReferenceFilter')
diff --git a/lib/banzai/filter/alert_reference_filter.rb b/lib/banzai/filter/alert_reference_filter.rb
deleted file mode 100644
index 228a4159c99..00000000000
--- a/lib/banzai/filter/alert_reference_filter.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class AlertReferenceFilter < IssuableReferenceFilter
- self.reference_type = :alert
-
- def self.object_class
- AlertManagement::Alert
- end
-
- def self.object_sym
- :alert
- end
-
- def parent_records(parent, ids)
- parent.alert_management_alerts.where(iid: ids.to_a)
- end
-
- def url_for_object(alert, project)
- ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
- project,
- alert.iid,
- only_path: context[:only_path]
- )
- end
- end
- end
-end
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
deleted file mode 100644
index d6b46236a49..00000000000
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces commit range references with links.
- #
- # This filter supports cross-project references.
- class CommitRangeReferenceFilter < AbstractReferenceFilter
- self.reference_type = :commit_range
-
- def self.object_class
- CommitRange
- end
-
- def self.references_in(text, pattern = CommitRange.reference_pattern)
- text.gsub(pattern) do |match|
- yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
- end
- end
-
- def initialize(*args)
- super
-
- @commit_map = {}
- end
-
- def find_object(project, id)
- return unless project.is_a?(Project)
-
- range = CommitRange.new(id, project)
-
- range.valid_commits? ? range : nil
- end
-
- def url_for_object(range, project)
- h = Gitlab::Routing.url_helpers
- h.project_compare_url(project,
- range.to_param.merge(only_path: context[:only_path]))
- end
-
- def object_link_title(range, matches)
- nil
- end
- end
- end
-end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
deleted file mode 100644
index 3df003a88fa..00000000000
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces commit references with links.
- #
- # This filter supports cross-project references.
- class CommitReferenceFilter < AbstractReferenceFilter
- self.reference_type = :commit
-
- def self.object_class
- Commit
- end
-
- def self.references_in(text, pattern = Commit.reference_pattern)
- text.gsub(pattern) do |match|
- yield match, $~[:commit], $~[:project], $~[:namespace], $~
- end
- end
-
- def find_object(project, id)
- return unless project.is_a?(Project) && project.valid_repo?
-
- _, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
-
- record
- end
-
- def referenced_merge_request_commit_shas
- return [] unless noteable.is_a?(MergeRequest)
-
- @referenced_merge_request_commit_shas ||= begin
- referenced_shas = references_per_parent.values.reduce(:|).to_a
- noteable.all_commit_shas.select do |sha|
- referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
- end
- end
- end
-
- # The default behaviour is `#to_i` - we just pass the hash through.
- def self.parse_symbol(sha_hash, _match)
- sha_hash
- end
-
- def url_for_object(commit, project)
- h = Gitlab::Routing.url_helpers
-
- if referenced_merge_request_commit_shas.include?(commit.id)
- h.diffs_project_merge_request_url(project,
- noteable,
- commit_id: commit.id,
- only_path: only_path?)
- else
- h.project_commit_url(project,
- commit,
- only_path: only_path?)
- end
- end
-
- def object_link_text_extras(object, matches)
- extras = super
-
- path = matches[:path] if matches.names.include?("path")
- if path == '/builds'
- extras.unshift "builds"
- end
-
- extras
- end
-
- private
-
- def parent_records(parent, ids)
- parent.commits_by(oids: ids.to_a)
- end
-
- def noteable
- context[:noteable]
- end
-
- def only_path?
- context[:only_path]
- end
- end
- end
-end
diff --git a/lib/banzai/filter/design_reference_filter.rb b/lib/banzai/filter/design_reference_filter.rb
deleted file mode 100644
index 1754fec93d4..00000000000
--- a/lib/banzai/filter/design_reference_filter.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class DesignReferenceFilter < AbstractReferenceFilter
- class Identifier
- include Comparable
- attr_reader :issue_iid, :filename
-
- def initialize(issue_iid:, filename:)
- @issue_iid = issue_iid
- @filename = filename
- end
-
- def as_composite_id(id_for_iid)
- id = id_for_iid[issue_iid]
- return unless id
-
- { issue_id: id, filename: filename }
- end
-
- def <=>(other)
- return unless other.is_a?(Identifier)
-
- [issue_iid, filename] <=> [other.issue_iid, other.filename]
- end
- alias_method :eql?, :==
-
- def hash
- [issue_iid, filename].hash
- end
- end
-
- self.reference_type = :design
-
- def find_object(project, identifier)
- records_per_parent[project][identifier]
- end
-
- def parent_records(project, identifiers)
- return [] unless project.design_management_enabled?
-
- iids = identifiers.map(&:issue_iid).to_set
- issues = project.issues.where(iid: iids)
- id_for_iid = issues.index_by(&:iid).transform_values(&:id)
- issue_by_id = issues.index_by(&:id)
-
- designs(identifiers, id_for_iid).each do |d|
- issue = issue_by_id[d.issue_id]
- # optimisation: assign values we have already fetched
- d.project = project
- d.issue = issue
- end
- end
-
- def relation_for_paths(paths)
- super.includes(:route, :namespace, :group)
- end
-
- def parent_type
- :project
- end
-
- # optimisation to reuse the parent_per_reference query information
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
- end
-
- def url_for_object(design, project)
- path_options = { vueroute: design.filename }
- Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
- end
-
- def data_attributes_for(_text, _project, design, **_kwargs)
- super.merge(issue: design.issue_id)
- end
-
- def self.object_class
- ::DesignManagement::Design
- end
-
- def self.object_sym
- :design
- end
-
- def self.parse_symbol(raw, match_data)
- filename = match_data[:url_filename]
- iid = match_data[:issue].to_i
- Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
- end
-
- def record_identifier(design)
- Identifier.new(filename: design.filename, issue_iid: design.issue.iid)
- end
-
- private
-
- def designs(identifiers, id_for_iid)
- identifiers
- .map { |identifier| identifier.as_composite_id(id_for_iid) }
- .compact
- .in_groups_of(100, false) # limitation of by_issue_id_and_filename, so we batch
- .flat_map { |ids| DesignManagement::Design.by_issue_id_and_filename(ids) }
- end
- end
- end
-end
diff --git a/lib/banzai/filter/epic_reference_filter.rb b/lib/banzai/filter/epic_reference_filter.rb
deleted file mode 100644
index 70a6cb0a6dc..00000000000
--- a/lib/banzai/filter/epic_reference_filter.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # The actual filter is implemented in the EE mixin
- class EpicReferenceFilter < IssuableReferenceFilter
- self.reference_type = :epic
-
- def self.object_class
- Epic
- end
-
- private
-
- def group
- context[:group] || context[:project]&.group
- end
- end
- end
-end
-
-Banzai::Filter::EpicReferenceFilter.prepend_if_ee('EE::Banzai::Filter::EpicReferenceFilter')
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
deleted file mode 100644
index fcf4863ab4f..00000000000
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces external issue tracker references with links.
- # References are ignored if the project doesn't use an external issue
- # tracker.
- #
- # This filter does not support cross-project references.
- class ExternalIssueReferenceFilter < ReferenceFilter
- self.reference_type = :external_issue
-
- # Public: Find `JIRA-123` issue references in text
- #
- # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
- # "<a href=...>##{issue}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match and the String issue reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text, pattern)
- text.gsub(pattern) do |match|
- yield match, $~[:issue]
- end
- end
-
- def call
- # Early return if the project isn't using an external tracker
- return doc if project.nil? || default_issues_tracker?
-
- ref_pattern = issue_reference_pattern
- ref_start_pattern = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- issue_link_filter(content)
- end
-
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_start_pattern
- replace_link_node_with_href(node, index, link) do
- issue_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
- end
-
- private
-
- # Replace `JIRA-123` issue references in text with links to the referenced
- # issue's details page.
- #
- # text - String text to replace references in.
- # link_content - Original content of the link being replaced.
- #
- # Returns a String with `JIRA-123` references replaced with links. All
- # links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text, link_content: nil)
- self.class.references_in(text, issue_reference_pattern) do |match, id|
- url = url_for_issue(id)
- klass = reference_class(:issue)
- data = data_attribute(project: project.id, external_issue: id)
- content = link_content || match
-
- %(<a href="#{url}" #{data}
- title="#{escape_once(issue_title)}"
- class="#{klass}">#{content}</a>)
- end
- end
-
- def url_for_issue(issue_id)
- return '' if project.nil?
-
- url = if only_path?
- project.external_issue_tracker.issue_path(issue_id)
- else
- project.external_issue_tracker.issue_url(issue_id)
- end
-
- # Ensure we return a valid URL to prevent possible XSS.
- URI.parse(url).to_s
- rescue URI::InvalidURIError
- ''
- end
-
- def default_issues_tracker?
- external_issues_cached(:default_issues_tracker?)
- end
-
- def issue_reference_pattern
- external_issues_cached(:external_issue_reference_pattern)
- end
-
- def project
- context[:project]
- end
-
- def issue_title
- "Issue in #{project.external_issue_tracker.title}"
- end
-
- def external_issues_cached(attribute)
- cached_attributes = Gitlab::SafeRequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
- cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend
- cached_attributes[project.id][attribute]
- end
- end
- end
-end
diff --git a/lib/banzai/filter/feature_flag_reference_filter.rb b/lib/banzai/filter/feature_flag_reference_filter.rb
deleted file mode 100644
index c11576901ce..00000000000
--- a/lib/banzai/filter/feature_flag_reference_filter.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class FeatureFlagReferenceFilter < IssuableReferenceFilter
- self.reference_type = :feature_flag
-
- def self.object_class
- Operations::FeatureFlag
- end
-
- def self.object_sym
- :feature_flag
- end
-
- def parent_records(parent, ids)
- parent.operations_feature_flags.where(iid: ids.to_a)
- end
-
- def url_for_object(feature_flag, project)
- ::Gitlab::Routing.url_helpers.edit_project_feature_flag_url(
- project,
- feature_flag.iid,
- only_path: context[:only_path]
- )
- end
-
- def object_link_title(object, matches)
- object.name
- end
- end
- end
-end
diff --git a/lib/banzai/filter/issuable_reference_filter.rb b/lib/banzai/filter/issuable_reference_filter.rb
deleted file mode 100644
index b91ba9f7256..00000000000
--- a/lib/banzai/filter/issuable_reference_filter.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class IssuableReferenceFilter < AbstractReferenceFilter
- def record_identifier(record)
- record.iid.to_i
- end
-
- def find_object(parent, iid)
- records_per_parent[parent][iid]
- end
-
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
- end
- end
- end
-end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
deleted file mode 100644
index 216418ee5fa..00000000000
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces issue references with links. References to
- # issues that do not exist are ignored.
- #
- # This filter supports cross-project references.
- #
- # When external issues tracker like Jira is activated we should not
- # use issue reference pattern, but we should still be able
- # to reference issues from other GitLab projects.
- class IssueReferenceFilter < IssuableReferenceFilter
- self.reference_type = :issue
-
- def self.object_class
- Issue
- end
-
- def url_for_object(issue, project)
- return issue_path(issue, project) if only_path?
-
- issue_url(issue, project)
- end
-
- def parent_records(parent, ids)
- parent.issues.where(iid: ids.to_a)
- end
-
- def object_link_text_extras(issue, matches)
- super + design_link_extras(issue, matches.named_captures['path'])
- end
-
- private
-
- def issue_path(issue, project)
- Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid)
- end
-
- def issue_url(issue, project)
- Gitlab::Routing.url_helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue.iid)
- end
-
- def design_link_extras(issue, path)
- if path == '/designs' && read_designs?(issue)
- ['designs']
- else
- []
- end
- end
-
- def read_designs?(issue)
- issue.project.design_management_enabled?
- end
- end
- end
-end
diff --git a/lib/banzai/filter/iteration_reference_filter.rb b/lib/banzai/filter/iteration_reference_filter.rb
deleted file mode 100644
index 9d2b533e6da..00000000000
--- a/lib/banzai/filter/iteration_reference_filter.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # The actual filter is implemented in the EE mixin
- class IterationReferenceFilter < AbstractReferenceFilter
- self.reference_type = :iteration
-
- def self.object_class
- Iteration
- end
- end
- end
-end
-
-Banzai::Filter::IterationReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IterationReferenceFilter')
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
deleted file mode 100644
index a4d3e352051..00000000000
--- a/lib/banzai/filter/label_reference_filter.rb
+++ /dev/null
@@ -1,129 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces label references with links.
- class LabelReferenceFilter < AbstractReferenceFilter
- self.reference_type = :label
-
- def self.object_class
- Label
- end
-
- def find_object(parent_object, id)
- find_labels(parent_object).find(id)
- end
-
- def references_in(text, pattern = Label.reference_pattern)
- labels = {}
- unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
- namespace, project = $~[:namespace], $~[:project]
- project_path = full_project_path(namespace, project)
- label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
-
- if label
- labels[label.id] = yield match, label.id, project, namespace, $~
- "#{REFERENCE_PLACEHOLDER}#{label.id}"
- else
- match
- end
- end
-
- return text if labels.empty?
-
- escape_with_placeholders(unescaped_html, labels)
- end
-
- def find_label_cached(parent_ref, label_id, label_name)
- cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do
- find_label(parent_ref, label_id, label_name)
- end
- end
-
- def find_label(parent_ref, label_id, label_name)
- parent = parent_from_ref(parent_ref)
- return unless parent
-
- label_params = label_params(label_id, label_name)
- find_labels(parent).find_by(label_params)
- end
-
- def find_labels(parent)
- params = if parent.is_a?(Group)
- { group_id: parent.id,
- include_ancestor_groups: true,
- only_group_labels: true }
- else
- { project: parent,
- include_ancestor_groups: true }
- end
-
- LabelsFinder.new(nil, params).execute(skip_authorization: true)
- end
-
- # Parameters to pass to `Label.find_by` based on the given arguments
- #
- # id - Integer ID to pass. If present, returns {id: id}
- # name - String name to pass. If `id` is absent, finds by name without
- # surrounding quotes.
- #
- # Returns a Hash.
- def label_params(id, name)
- if name
- { name: name.tr('"', '') }
- else
- { id: id.to_i }
- end
- end
-
- def url_for_object(label, parent)
- label_url_method =
- if context[:label_url_method]
- context[:label_url_method]
- elsif parent.is_a?(Project)
- :project_issues_url
- end
-
- return unless label_url_method
-
- Gitlab::Routing.url_helpers.public_send(label_url_method, parent, label_name: label.name, only_path: context[:only_path]) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def object_link_text(object, matches)
- label_suffix = ''
- parent = project || group
-
- if project || full_path_ref?(matches)
- project_path = full_project_path(matches[:namespace], matches[:project])
- parent_from_ref = from_ref_cached(project_path)
- reference = parent_from_ref.to_human_reference(parent)
-
- label_suffix = " <i>in #{ERB::Util.html_escape(reference)}</i>" if reference.present?
- end
-
- presenter = object.present(issuable_subject: parent)
- LabelsHelper.render_colored_label(presenter, suffix: label_suffix)
- end
-
- def wrap_link(link, label)
- presenter = label.present(issuable_subject: project || group)
- LabelsHelper.wrap_label_html(link, small: true, label: presenter)
- end
-
- def full_path_ref?(matches)
- matches[:namespace] && matches[:project]
- end
-
- def reference_class(type, tooltip: true)
- super + ' gl-link gl-label-link'
- end
-
- def object_link_title(object, matches)
- presenter = object.present(issuable_subject: project || group)
- LabelsHelper.label_tooltip_title(presenter)
- end
- end
- end
-end
-
-Banzai::Filter::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::LabelReferenceFilter')
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
deleted file mode 100644
index 0b8bd17a71b..00000000000
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces merge request references with links. References
- # to merge requests that do not exist are ignored.
- #
- # This filter supports cross-project references.
- class MergeRequestReferenceFilter < IssuableReferenceFilter
- self.reference_type = :merge_request
-
- def self.object_class
- MergeRequest
- end
-
- def url_for_object(mr, project)
- h = Gitlab::Routing.url_helpers
- h.project_merge_request_url(project, mr,
- only_path: context[:only_path])
- end
-
- def object_link_title(object, matches)
- # The method will return `nil` if object is not a commit
- # allowing for properly handling the extended MR Tooltip
- object_link_commit_title(object, matches)
- end
-
- def object_link_text_extras(object, matches)
- extras = super
-
- if commit_ref = object_link_commit_ref(object, matches)
- klass = reference_class(:commit, tooltip: false)
- commit_ref_tag = %(<span class="#{klass}">#{commit_ref}</span>)
-
- return extras.unshift(commit_ref_tag)
- end
-
- path = matches[:path] if matches.names.include?("path")
-
- case path
- when '/diffs'
- extras.unshift "diffs"
- when '/commits'
- extras.unshift "commits"
- when '/builds'
- extras.unshift "builds"
- end
-
- extras
- end
-
- def parent_records(parent, ids)
- parent.merge_requests
- .where(iid: ids.to_a)
- .includes(target_project: :namespace)
- end
-
- def reference_class(object_sym, options = {})
- super(object_sym, tooltip: false)
- end
-
- def data_attributes_for(text, parent, object, **data)
- super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
- end
-
- private
-
- def object_link_commit_title(object, matches)
- object_link_commit(object, matches)&.title
- end
-
- def object_link_commit_ref(object, matches)
- object_link_commit(object, matches)&.short_id
- end
-
- def object_link_commit(object, matches)
- return unless matches.names.include?('query') && query = matches[:query]
-
- # Removes leading "?". CGI.parse expects "arg1&arg2&arg3"
- params = CGI.parse(query.sub(/^\?/, ''))
-
- return unless commit_sha = params['commit_id']&.first
-
- if commit = find_commit_by_sha(object, commit_sha)
- Commit.from_hash(commit.to_hash, object.project)
- end
- end
-
- def find_commit_by_sha(object, commit_sha)
- @all_commits ||= {}
- @all_commits[object.id] ||= object.all_commits
-
- @all_commits[object.id].find { |commit| commit.sha == commit_sha }
- end
- end
- end
-end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
deleted file mode 100644
index 126208db935..00000000000
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces milestone references with links.
- class MilestoneReferenceFilter < AbstractReferenceFilter
- include Gitlab::Utils::StrongMemoize
-
- self.reference_type = :milestone
-
- def self.object_class
- Milestone
- end
-
- # Links to project milestones contain the IID, but when we're handling
- # 'regular' references, we need to use the global ID to disambiguate
- # between group and project milestones.
- def find_object(parent, id)
- return unless valid_context?(parent)
-
- find_milestone_with_finder(parent, id: id)
- end
-
- def find_object_from_link(parent, iid)
- return unless valid_context?(parent)
-
- find_milestone_with_finder(parent, iid: iid)
- end
-
- def valid_context?(parent)
- strong_memoize(:valid_context) do
- group_context?(parent) || project_context?(parent)
- end
- end
-
- def group_context?(parent)
- strong_memoize(:group_context) do
- parent.is_a?(Group)
- end
- end
-
- def project_context?(parent)
- strong_memoize(:project_context) do
- parent.is_a?(Project)
- end
- end
-
- def references_in(text, pattern = Milestone.reference_pattern)
- # We'll handle here the references that follow the `reference_pattern`.
- # Other patterns (for example, the link pattern) are handled by the
- # default implementation.
- return super(text, pattern) if pattern != Milestone.reference_pattern
-
- milestones = {}
- unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
- milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
-
- if milestone
- milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
- "#{REFERENCE_PLACEHOLDER}#{milestone.id}"
- else
- match
- end
- end
-
- return text if milestones.empty?
-
- escape_with_placeholders(unescaped_html, milestones)
- end
-
- def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
- project_path = full_project_path(namespace_ref, project_ref)
-
- # Returns group if project is not found by path
- parent = parent_from_ref(project_path)
-
- return unless parent
-
- milestone_params = milestone_params(milestone_id, milestone_name)
-
- find_milestone_with_finder(parent, milestone_params)
- end
-
- def milestone_params(iid, name)
- if name
- { name: name.tr('"', '') }
- else
- { iid: iid.to_i }
- end
- end
-
- def find_milestone_with_finder(parent, params)
- finder_params = milestone_finder_params(parent, params[:iid].present?)
-
- MilestonesFinder.new(finder_params).find_by(params)
- end
-
- def milestone_finder_params(parent, find_by_iid)
- { order: nil, state: 'all' }.tap do |params|
- params[:project_ids] = parent.id if project_context?(parent)
-
- # We don't support IID lookups because IIDs can clash between
- # group/project milestones and group/subgroup milestones.
- params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid
- end
- end
-
- def self_and_ancestors_ids(parent)
- if group_context?(parent)
- parent.self_and_ancestors.select(:id)
- elsif project_context?(parent)
- parent.group&.self_and_ancestors&.select(:id)
- end
- end
-
- def url_for_object(milestone, project)
- Gitlab::Routing
- .url_helpers
- .milestone_url(milestone, only_path: context[:only_path])
- end
-
- def object_link_text(object, matches)
- milestone_link = escape_once(super)
- reference = object.project&.to_reference_base(project)
-
- if reference.present?
- "#{milestone_link} <i>in #{reference}</i>".html_safe
- else
- milestone_link
- end
- end
-
- def object_link_title(object, matches)
- nil
- end
- end
- end
-end
diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb
deleted file mode 100644
index 50e23460cb8..00000000000
--- a/lib/banzai/filter/project_reference_filter.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces project references with links.
- class ProjectReferenceFilter < ReferenceFilter
- self.reference_type = :project
-
- # Public: Find `namespace/project>` project references in text
- #
- # ProjectReferenceFilter.references_in(text) do |match, project|
- # "<a href=...>#{project}></a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, and the String project name.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Project.markdown_reference_pattern) do |match|
- yield match, "#{$~[:namespace]}/#{$~[:project]}"
- end
- end
-
- def call
- ref_pattern = Project.markdown_reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- project_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- project_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
- end
-
- # Replace `namespace/project>` project references in text with links to the referenced
- # project page.
- #
- # text - String text to replace references in.
- # link_content - Original content of the link being replaced.
- #
- # Returns a String with `namespace/project>` references replaced with links. All links
- # have `gfm` and `gfm-project` class names attached for styling.
- def project_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, project_path|
- cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
- if project = projects_hash[project_path.downcase]
- link_to_project(project, link_content: link_content) || match
- else
- match
- end
- end
- end
- end
-
- # Returns a Hash containing all Project objects for the project
- # references in the current document.
- #
- # The keys of this Hash are the project paths, the values the
- # corresponding Project objects.
- def projects_hash
- @projects ||= Project.eager_load(:route, namespace: [:route])
- .where_full_path_in(projects)
- .index_by(&:full_path)
- .transform_keys(&:downcase)
- end
-
- # Returns all projects referenced in the current document.
- def projects
- refs = Set.new
-
- nodes.each do |node|
- node.to_html.scan(Project.markdown_reference_pattern) do
- refs << "#{$~[:namespace]}/#{$~[:project]}"
- end
- end
-
- refs.to_a
- end
-
- private
-
- def urls
- Gitlab::Routing.url_helpers
- end
-
- def link_class
- reference_class(:project)
- end
-
- def link_to_project(project, link_content: nil)
- url = urls.project_url(project, only_path: context[:only_path])
- data = data_attribute(project: project.id)
- content = link_content || project.to_reference
-
- link_tag(url, data, content, project.name)
- end
-
- def link_tag(url, data, link_content, title)
- %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
- end
- end
- end
-end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
deleted file mode 100644
index d22a0e0b504..00000000000
--- a/lib/banzai/filter/reference_filter.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-# frozen_string_literal: true
-
-# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js
-module Banzai
- module Filter
- # Base class for GitLab Flavored Markdown reference filters.
- #
- # References within <pre>, <code>, <a>, and <style> elements are ignored.
- #
- # Context options:
- # :project (required) - Current project, ignored if reference is cross-project.
- # :only_path - Generate path-only links.
- class ReferenceFilter < HTML::Pipeline::Filter
- include RequestStoreReferenceCache
- include OutputSafety
-
- class << self
- attr_accessor :reference_type
-
- def call(doc, context = nil, result = nil)
- new(doc, context, result).call_and_update_nodes
- end
- end
-
- def initialize(doc, context = nil, result = nil)
- super
-
- @new_nodes = {}
- @nodes = self.result[:reference_filter_nodes]
- end
-
- def call_and_update_nodes
- with_update_nodes { call }
- end
-
- # Returns a data attribute String to attach to a reference link
- #
- # attributes - Hash, where the key becomes the data attribute name and the
- # value is the data attribute value
- #
- # Examples:
- #
- # data_attribute(project: 1, issue: 2)
- # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
- #
- # data_attribute(project: 3, merge_request: 4)
- # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
- #
- # Returns a String
- def data_attribute(attributes = {})
- attributes = attributes.reject { |_, v| v.nil? }
-
- attributes[:reference_type] ||= self.class.reference_type
- attributes[:container] ||= 'body'
- attributes[:placement] ||= 'top'
- attributes.delete(:original) if context[:no_original_data]
- attributes.map do |key, value|
- %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
- end.join(' ')
- end
-
- def ignore_ancestor_query
- @ignore_ancestor_query ||= begin
- parents = %w(pre code a style)
- parents << 'blockquote' if context[:ignore_blockquotes]
-
- parents.map { |n| "ancestor::#{n}" }.join(' or ')
- end
- end
-
- def project
- context[:project]
- end
-
- def group
- context[:group]
- end
-
- def user
- context[:user]
- end
-
- def skip_project_check?
- context[:skip_project_check]
- end
-
- def reference_class(type, tooltip: true)
- gfm_klass = "gfm gfm-#{type}"
-
- return gfm_klass unless tooltip
-
- "#{gfm_klass} has-tooltip"
- end
-
- # Ensure that a :project key exists in context
- #
- # Note that while the key might exist, its value could be nil!
- def validate
- needs :project unless skip_project_check?
- end
-
- # Iterates over all <a> and text() nodes in a document.
- #
- # Nodes are skipped whenever their ancestor is one of the nodes returned
- # by `ignore_ancestor_query`. Link tags are not processed if they have a
- # "gfm" class or the "href" attribute is empty.
- def each_node
- return to_enum(__method__) unless block_given?
-
- doc.xpath(query).each do |node|
- yield node
- end
- end
-
- # Returns an Array containing all HTML nodes.
- def nodes
- @nodes ||= each_node.to_a
- end
-
- # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
- def yield_valid_link(node)
- link = unescape_link(node.attr('href').to_s)
- inner_html = node.inner_html
-
- return unless link.force_encoding('UTF-8').valid_encoding?
-
- yield link, inner_html
- end
-
- def unescape_link(href)
- CGI.unescape(href)
- end
-
- def replace_text_when_pattern_matches(node, index, pattern)
- return unless node.text =~ pattern
-
- content = node.to_html
- html = yield content
-
- replace_text_with_html(node, index, html) unless html == content
- end
-
- def replace_link_node_with_text(node, index)
- html = yield
-
- replace_text_with_html(node, index, html) unless html == node.text
- end
-
- def replace_link_node_with_href(node, index, link)
- html = yield
-
- replace_text_with_html(node, index, html) unless html == link
- end
-
- def text_node?(node)
- node.is_a?(Nokogiri::XML::Text)
- end
-
- def element_node?(node)
- node.is_a?(Nokogiri::XML::Element)
- end
-
- private
-
- def query
- @query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
- | descendant-or-self::a[
- not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
- ]}
- end
-
- def replace_text_with_html(node, index, html)
- replace_and_update_new_nodes(node, index, html)
- end
-
- def replace_and_update_new_nodes(node, index, html)
- previous_node = node.previous
- next_node = node.next
- parent_node = node.parent
- # Unfortunately node.replace(html) returns re-parented nodes, not the actual replaced nodes in the doc
- # We need to find the actual nodes in the doc that were replaced
- node.replace(html)
- @new_nodes[index] = []
-
- # We replaced node with new nodes, so we find first new node. If previous_node is nil, we take first parent child
- new_node = previous_node ? previous_node.next : parent_node&.children&.first
-
- # We iterate from first to last replaced node and store replaced nodes in @new_nodes
- while new_node && new_node != next_node
- @new_nodes[index] << new_node.xpath(query)
- new_node = new_node.next
- end
-
- @new_nodes[index].flatten!
- end
-
- def only_path?
- context[:only_path]
- end
-
- def with_update_nodes
- @new_nodes = {}
- yield.tap { update_nodes! }
- end
-
- # Once Filter completes replacing nodes, we update nodes with @new_nodes
- def update_nodes!
- @new_nodes.sort_by { |index, _new_nodes| -index }.each do |index, new_nodes|
- nodes[index, 1] = new_nodes
- end
- result[:reference_filter_nodes] = nodes
- end
- end
- end
-end
diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb
new file mode 100644
index 00000000000..7109373dbce
--- /dev/null
+++ b/lib/banzai/filter/references/abstract_reference_filter.rb
@@ -0,0 +1,448 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # Issues, merge requests, Snippets, Commits and Commit Ranges share
+ # similar functionality in reference filtering.
+ class AbstractReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
+ # reference (which we replace with placeholder during re-scaping). The
+ # random number helps ensure it's pretty close to unique. Since it's a
+ # transitory value (it never gets saved) we can initialize once, and it
+ # doesn't matter if it changes on a restart.
+ REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
+ REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
+
+ def self.object_class
+ # Implement in child class
+ # Example: MergeRequest
+ end
+
+ def self.object_name
+ @object_name ||= object_class.name.underscore
+ end
+
+ def self.object_sym
+ @object_sym ||= object_name.to_sym
+ end
+
+ # Public: Find references in text (like `!123` for merge requests)
+ #
+ # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
+ # object = find_object(project_ref, id)
+ # "<a href=...>#{object.to_reference}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer referenced object ID, an optional String
+ # of the external project reference, and all of the matchdata.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text, pattern = object_class.reference_pattern)
+ text.gsub(pattern) do |match|
+ if ident = identifier($~)
+ yield match, ident, $~[:project], $~[:namespace], $~
+ else
+ match
+ end
+ end
+ end
+
+ def self.identifier(match_data)
+ symbol = symbol_from_match(match_data)
+
+ parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
+ end
+
+ def identifier(match_data)
+ self.class.identifier(match_data)
+ end
+
+ def self.symbol_from_match(match)
+ key = object_sym
+ match[key] if match.names.include?(key.to_s)
+ end
+
+ # Transform a symbol extracted from the text to a meaningful value
+ # In most cases these will be integers, so we call #to_i by default
+ #
+ # This method has the contract that if a string `ref` refers to a
+ # record `record`, then `parse_symbol(ref) == record_identifier(record)`.
+ def self.parse_symbol(symbol, match_data)
+ symbol.to_i
+ end
+
+ # We assume that most classes are identifying records by ID.
+ #
+ # This method has the contract that if a string `ref` refers to a
+ # record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
+ def record_identifier(record)
+ record.id
+ end
+
+ def object_class
+ self.class.object_class
+ end
+
+ def object_sym
+ self.class.object_sym
+ end
+
+ def references_in(*args, &block)
+ self.class.references_in(*args, &block)
+ end
+
+ # Implement in child class
+ # Example: project.merge_requests.find
+ def find_object(parent_object, id)
+ end
+
+ # Override if the link reference pattern produces a different ID (global
+ # ID vs internal ID, for instance) to the regular reference pattern.
+ def find_object_from_link(parent_object, id)
+ find_object(parent_object, id)
+ end
+
+ # Implement in child class
+ # Example: project_merge_request_url
+ def url_for_object(object, parent_object)
+ end
+
+ def find_object_cached(parent_object, id)
+ cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do
+ find_object(parent_object, id)
+ end
+ end
+
+ def find_object_from_link_cached(parent_object, id)
+ cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do
+ find_object_from_link(parent_object, id)
+ end
+ end
+
+ def from_ref_cached(ref)
+ cached_call("banzai_#{parent_type}_refs".to_sym, ref) do
+ parent_from_ref(ref)
+ end
+ end
+
+ def url_for_object_cached(object, parent_object)
+ cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do
+ url_for_object(object, parent_object)
+ end
+ end
+
+ def call
+ return doc unless project || group || user
+
+ ref_pattern = object_class.reference_pattern
+ link_pattern = object_class.link_reference_pattern
+
+ # Compile often used regexps only once outside of the loop
+ ref_pattern_anchor = /\A#{ref_pattern}\z/
+ link_pattern_start = /\A#{link_pattern}/
+ link_pattern_anchor = /\A#{link_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node) && ref_pattern
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ object_link_filter(content, ref_pattern)
+ end
+
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if ref_pattern && link =~ ref_pattern_anchor
+ replace_link_node_with_href(node, index, link) do
+ object_link_filter(link, ref_pattern, link_content: inner_html)
+ end
+
+ next
+ end
+
+ next unless link_pattern
+
+ if link == inner_html && inner_html =~ link_pattern_start
+ replace_link_node_with_text(node, index) do
+ object_link_filter(inner_html, link_pattern, link_reference: true)
+ end
+
+ next
+ end
+
+ if link =~ link_pattern_anchor
+ replace_link_node_with_href(node, index, link) do
+ object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
+ end
+
+ next
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Replace references (like `!123` for merge requests) in text with links
+ # to the referenced object's details page.
+ #
+ # text - String text to replace references in.
+ # pattern - Reference pattern to match against.
+ # link_content - Original content of the link being replaced.
+ # link_reference - True if this was using the link reference pattern,
+ # false otherwise.
+ #
+ # Returns a String with references replaced with links. All links
+ # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
+ parent_path = if parent_type == :group
+ full_group_path(namespace_ref)
+ else
+ full_project_path(namespace_ref, project_ref)
+ end
+
+ parent = from_ref_cached(parent_path)
+
+ if parent
+ object =
+ if link_reference
+ find_object_from_link_cached(parent, id)
+ else
+ find_object_cached(parent, id)
+ end
+ end
+
+ if object
+ title = object_link_title(object, matches)
+ klass = reference_class(object_sym)
+
+ data_attributes = data_attributes_for(link_content || match, parent, object,
+ link_content: !!link_content,
+ link_reference: link_reference)
+ data = data_attribute(data_attributes)
+
+ url =
+ if matches.names.include?("url") && matches[:url]
+ matches[:url]
+ else
+ url_for_object_cached(object, parent)
+ end
+
+ content = link_content || object_link_text(object, matches)
+
+ link = %(<a href="#{url}" #{data}
+ title="#{escape_once(title)}"
+ class="#{klass}">#{content}</a>)
+
+ wrap_link(link, object)
+ else
+ match
+ end
+ end
+ end
+
+ def wrap_link(link, object)
+ link
+ end
+
+ def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
+ object_parent_type = parent.is_a?(Group) ? :group : :project
+
+ {
+ original: escape_html_entities(text),
+ link: link_content,
+ link_reference: link_reference,
+ object_parent_type => parent.id,
+ object_sym => object.id
+ }
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = []
+
+ if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
+ extras << "comment #{Regexp.last_match(1)}"
+ end
+
+ extension = matches[:extension] if matches.names.include?("extension")
+
+ extras << extension if extension
+
+ extras
+ end
+
+ def object_link_title(object, matches)
+ object.title
+ end
+
+ def object_link_text(object, matches)
+ parent = project || group || user
+ text = object.reference_link_text(parent)
+
+ extras = object_link_text_extras(object, matches)
+ text += " (#{extras.join(", ")})" if extras.any?
+
+ text
+ end
+
+ # Returns a Hash containing all object references (e.g. issue IDs) per the
+ # project they belong to.
+ def references_per_parent
+ @references_per ||= {}
+
+ @references_per[parent_type] ||= begin
+ refs = Hash.new { |hash, key| hash[key] = Set.new }
+ regex = [
+ object_class.link_reference_pattern,
+ object_class.reference_pattern
+ ].compact.reduce { |a, b| Regexp.union(a, b) }
+
+ nodes.each do |node|
+ node.to_html.scan(regex) do
+ path = if parent_type == :project
+ full_project_path($~[:namespace], $~[:project])
+ else
+ full_group_path($~[:group])
+ end
+
+ if ident = identifier($~)
+ refs[path] << ident
+ end
+ end
+ end
+
+ refs
+ end
+ end
+
+ # Returns a Hash containing referenced projects grouped per their full
+ # path.
+ def parent_per_reference
+ @per_reference ||= {}
+
+ @per_reference[parent_type] ||= begin
+ refs = Set.new
+
+ references_per_parent.each do |ref, _|
+ refs << ref
+ end
+
+ find_for_paths(refs.to_a).index_by(&:full_path)
+ end
+ end
+
+ def relation_for_paths(paths)
+ klass = parent_type.to_s.camelize.constantize
+ result = klass.where_full_path_in(paths)
+ return result if parent_type == :group
+
+ result.includes(:namespace) if parent_type == :project
+ end
+
+ # Returns projects for the given paths.
+ def find_for_paths(paths)
+ if Gitlab::SafeRequestStore.active?
+ cache = refs_cache
+ to_query = paths - cache.keys
+
+ unless to_query.empty?
+ records = relation_for_paths(to_query)
+
+ found = []
+ records.each do |record|
+ ref = record.full_path
+ get_or_set_cache(cache, ref) { record }
+ found << ref
+ end
+
+ not_found = to_query - found
+ not_found.each do |ref|
+ get_or_set_cache(cache, ref) { nil }
+ end
+ end
+
+ cache.slice(*paths).values.compact
+ else
+ relation_for_paths(paths)
+ end
+ end
+
+ def current_parent_path
+ @current_parent_path ||= parent&.full_path
+ end
+
+ def current_project_namespace_path
+ @current_project_namespace_path ||= project&.namespace&.full_path
+ end
+
+ def records_per_parent
+ @_records_per_project ||= {}
+
+ @_records_per_project[object_class.to_s.underscore] ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ parent_per_reference.each do |path, parent|
+ record_ids = references_per_parent[path]
+
+ parent_records(parent, record_ids).each do |record|
+ hash[parent][record_identifier(record)] = record
+ end
+ end
+
+ hash
+ end
+ end
+
+ private
+
+ def full_project_path(namespace, project_ref)
+ return current_parent_path unless project_ref
+
+ namespace_ref = namespace || current_project_namespace_path
+ "#{namespace_ref}/#{project_ref}"
+ end
+
+ def refs_cache
+ Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
+ end
+
+ def parent_type
+ :project
+ end
+
+ def parent
+ parent_type == :project ? project : group
+ end
+
+ def full_group_path(group_ref)
+ return current_parent_path unless group_ref
+
+ group_ref
+ end
+
+ def unescape_html_entities(text)
+ CGI.unescapeHTML(text.to_s)
+ end
+
+ def escape_html_entities(text)
+ CGI.escapeHTML(text.to_s)
+ end
+
+ def escape_with_placeholders(text, placeholder_data)
+ escaped = escape_html_entities(text)
+
+ escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
+ placeholder_data[Regexp.last_match(1).to_i]
+ end
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::AbstractReferenceFilter')
diff --git a/lib/banzai/filter/references/alert_reference_filter.rb b/lib/banzai/filter/references/alert_reference_filter.rb
new file mode 100644
index 00000000000..90fef536605
--- /dev/null
+++ b/lib/banzai/filter/references/alert_reference_filter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class AlertReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :alert
+
+ def self.object_class
+ AlertManagement::Alert
+ end
+
+ def self.object_sym
+ :alert
+ end
+
+ def parent_records(parent, ids)
+ parent.alert_management_alerts.where(iid: ids.to_a)
+ end
+
+ def url_for_object(alert, project)
+ ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
+ project,
+ alert.iid,
+ only_path: context[:only_path]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/commit_range_reference_filter.rb b/lib/banzai/filter/references/commit_range_reference_filter.rb
new file mode 100644
index 00000000000..ad79f8a173c
--- /dev/null
+++ b/lib/banzai/filter/references/commit_range_reference_filter.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces commit range references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitRangeReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :commit_range
+
+ def self.object_class
+ CommitRange
+ end
+
+ def self.references_in(text, pattern = CommitRange.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
+ end
+ end
+
+ def initialize(*args)
+ super
+
+ @commit_map = {}
+ end
+
+ def find_object(project, id)
+ return unless project.is_a?(Project)
+
+ range = CommitRange.new(id, project)
+
+ range.valid_commits? ? range : nil
+ end
+
+ def url_for_object(range, project)
+ h = Gitlab::Routing.url_helpers
+ h.project_compare_url(project,
+ range.to_param.merge(only_path: context[:only_path]))
+ end
+
+ def object_link_title(range, matches)
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/commit_reference_filter.rb b/lib/banzai/filter/references/commit_reference_filter.rb
new file mode 100644
index 00000000000..457921bd07d
--- /dev/null
+++ b/lib/banzai/filter/references/commit_reference_filter.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces commit references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :commit
+
+ def self.object_class
+ Commit
+ end
+
+ def self.references_in(text, pattern = Commit.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit], $~[:project], $~[:namespace], $~
+ end
+ end
+
+ def find_object(project, id)
+ return unless project.is_a?(Project) && project.valid_repo?
+
+ _, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
+
+ record
+ end
+
+ def referenced_merge_request_commit_shas
+ return [] unless noteable.is_a?(MergeRequest)
+
+ @referenced_merge_request_commit_shas ||= begin
+ referenced_shas = references_per_parent.values.reduce(:|).to_a
+ noteable.all_commit_shas.select do |sha|
+ referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
+ end
+ end
+ end
+
+ # The default behaviour is `#to_i` - we just pass the hash through.
+ def self.parse_symbol(sha_hash, _match)
+ sha_hash
+ end
+
+ def url_for_object(commit, project)
+ h = Gitlab::Routing.url_helpers
+
+ if referenced_merge_request_commit_shas.include?(commit.id)
+ h.diffs_project_merge_request_url(project,
+ noteable,
+ commit_id: commit.id,
+ only_path: only_path?)
+ else
+ h.project_commit_url(project,
+ commit,
+ only_path: only_path?)
+ end
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = super
+
+ path = matches[:path] if matches.names.include?("path")
+ if path == '/builds'
+ extras.unshift "builds"
+ end
+
+ extras
+ end
+
+ private
+
+ def parent_records(parent, ids)
+ parent.commits_by(oids: ids.to_a)
+ end
+
+ def noteable
+ context[:noteable]
+ end
+
+ def only_path?
+ context[:only_path]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/design_reference_filter.rb b/lib/banzai/filter/references/design_reference_filter.rb
new file mode 100644
index 00000000000..61234e61c15
--- /dev/null
+++ b/lib/banzai/filter/references/design_reference_filter.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class DesignReferenceFilter < AbstractReferenceFilter
+ class Identifier
+ include Comparable
+ attr_reader :issue_iid, :filename
+
+ def initialize(issue_iid:, filename:)
+ @issue_iid = issue_iid
+ @filename = filename
+ end
+
+ def as_composite_id(id_for_iid)
+ id = id_for_iid[issue_iid]
+ return unless id
+
+ { issue_id: id, filename: filename }
+ end
+
+ def <=>(other)
+ return unless other.is_a?(Identifier)
+
+ [issue_iid, filename] <=> [other.issue_iid, other.filename]
+ end
+ alias_method :eql?, :==
+
+ def hash
+ [issue_iid, filename].hash
+ end
+ end
+
+ self.reference_type = :design
+
+ def find_object(project, identifier)
+ records_per_parent[project][identifier]
+ end
+
+ def parent_records(project, identifiers)
+ return [] unless project.design_management_enabled?
+
+ iids = identifiers.map(&:issue_iid).to_set
+ issues = project.issues.where(iid: iids)
+ id_for_iid = issues.index_by(&:iid).transform_values(&:id)
+ issue_by_id = issues.index_by(&:id)
+
+ designs(identifiers, id_for_iid).each do |d|
+ issue = issue_by_id[d.issue_id]
+ # optimisation: assign values we have already fetched
+ d.project = project
+ d.issue = issue
+ end
+ end
+
+ def relation_for_paths(paths)
+ super.includes(:route, :namespace, :group)
+ end
+
+ def parent_type
+ :project
+ end
+
+ # optimisation to reuse the parent_per_reference query information
+ def parent_from_ref(ref)
+ parent_per_reference[ref || current_parent_path]
+ end
+
+ def url_for_object(design, project)
+ path_options = { vueroute: design.filename }
+ Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
+ end
+
+ def data_attributes_for(_text, _project, design, **_kwargs)
+ super.merge(issue: design.issue_id)
+ end
+
+ def self.object_class
+ ::DesignManagement::Design
+ end
+
+ def self.object_sym
+ :design
+ end
+
+ def self.parse_symbol(raw, match_data)
+ filename = match_data[:url_filename]
+ iid = match_data[:issue].to_i
+ Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
+ end
+
+ def record_identifier(design)
+ Identifier.new(filename: design.filename, issue_iid: design.issue.iid)
+ end
+
+ private
+
+ def designs(identifiers, id_for_iid)
+ identifiers
+ .map { |identifier| identifier.as_composite_id(id_for_iid) }
+ .compact
+ .in_groups_of(100, false) # limitation of by_issue_id_and_filename, so we batch
+ .flat_map { |ids| DesignManagement::Design.by_issue_id_and_filename(ids) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/epic_reference_filter.rb b/lib/banzai/filter/references/epic_reference_filter.rb
new file mode 100644
index 00000000000..4ee446e5317
--- /dev/null
+++ b/lib/banzai/filter/references/epic_reference_filter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # The actual filter is implemented in the EE mixin
+ class EpicReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :epic
+
+ def self.object_class
+ Epic
+ end
+
+ private
+
+ def group
+ context[:group] || context[:project]&.group
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::EpicReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::EpicReferenceFilter')
diff --git a/lib/banzai/filter/references/external_issue_reference_filter.rb b/lib/banzai/filter/references/external_issue_reference_filter.rb
new file mode 100644
index 00000000000..247e20967df
--- /dev/null
+++ b/lib/banzai/filter/references/external_issue_reference_filter.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces external issue tracker references with links.
+ # References are ignored if the project doesn't use an external issue
+ # tracker.
+ #
+ # This filter does not support cross-project references.
+ class ExternalIssueReferenceFilter < ReferenceFilter
+ self.reference_type = :external_issue
+
+ # Public: Find `JIRA-123` issue references in text
+ #
+ # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match and the String issue reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text, pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:issue]
+ end
+ end
+
+ def call
+ # Early return if the project isn't using an external tracker
+ return doc if project.nil? || default_issues_tracker?
+
+ ref_pattern = issue_reference_pattern
+ ref_start_pattern = /\A#{ref_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ issue_link_filter(content)
+ end
+
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_start_pattern
+ replace_link_node_with_href(node, index, link) do
+ issue_link_filter(link, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ private
+
+ # Replace `JIRA-123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
+ #
+ # Returns a String with `JIRA-123` references replaced with links. All
+ # links have `gfm` and `gfm-issue` class names attached for styling.
+ def issue_link_filter(text, link_content: nil)
+ self.class.references_in(text, issue_reference_pattern) do |match, id|
+ url = url_for_issue(id)
+ klass = reference_class(:issue)
+ data = data_attribute(project: project.id, external_issue: id)
+ content = link_content || match
+
+ %(<a href="#{url}" #{data}
+ title="#{escape_once(issue_title)}"
+ class="#{klass}">#{content}</a>)
+ end
+ end
+
+ def url_for_issue(issue_id)
+ return '' if project.nil?
+
+ url = if only_path?
+ project.external_issue_tracker.issue_path(issue_id)
+ else
+ project.external_issue_tracker.issue_url(issue_id)
+ end
+
+ # Ensure we return a valid URL to prevent possible XSS.
+ URI.parse(url).to_s
+ rescue URI::InvalidURIError
+ ''
+ end
+
+ def default_issues_tracker?
+ external_issues_cached(:default_issues_tracker?)
+ end
+
+ def issue_reference_pattern
+ external_issues_cached(:external_issue_reference_pattern)
+ end
+
+ def project
+ context[:project]
+ end
+
+ def issue_title
+ "Issue in #{project.external_issue_tracker.title}"
+ end
+
+ def external_issues_cached(attribute)
+ cached_attributes = Gitlab::SafeRequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
+ cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend
+ cached_attributes[project.id][attribute]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/feature_flag_reference_filter.rb b/lib/banzai/filter/references/feature_flag_reference_filter.rb
new file mode 100644
index 00000000000..be9ded1ff43
--- /dev/null
+++ b/lib/banzai/filter/references/feature_flag_reference_filter.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class FeatureFlagReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :feature_flag
+
+ def self.object_class
+ Operations::FeatureFlag
+ end
+
+ def self.object_sym
+ :feature_flag
+ end
+
+ def parent_records(parent, ids)
+ parent.operations_feature_flags.where(iid: ids.to_a)
+ end
+
+ def url_for_object(feature_flag, project)
+ ::Gitlab::Routing.url_helpers.edit_project_feature_flag_url(
+ project,
+ feature_flag.iid,
+ only_path: context[:only_path]
+ )
+ end
+
+ def object_link_title(object, matches)
+ object.name
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/issuable_reference_filter.rb b/lib/banzai/filter/references/issuable_reference_filter.rb
new file mode 100644
index 00000000000..b8ccb926ae9
--- /dev/null
+++ b/lib/banzai/filter/references/issuable_reference_filter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class IssuableReferenceFilter < AbstractReferenceFilter
+ def record_identifier(record)
+ record.iid.to_i
+ end
+
+ def find_object(parent, iid)
+ records_per_parent[parent][iid]
+ end
+
+ def parent_from_ref(ref)
+ parent_per_reference[ref || current_parent_path]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/issue_reference_filter.rb b/lib/banzai/filter/references/issue_reference_filter.rb
new file mode 100644
index 00000000000..eacf261b15f
--- /dev/null
+++ b/lib/banzai/filter/references/issue_reference_filter.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces issue references with links. References to
+ # issues that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ #
+ # When external issues tracker like Jira is activated we should not
+ # use issue reference pattern, but we should still be able
+ # to reference issues from other GitLab projects.
+ class IssueReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :issue
+
+ def self.object_class
+ Issue
+ end
+
+ def url_for_object(issue, project)
+ return issue_path(issue, project) if only_path?
+
+ issue_url(issue, project)
+ end
+
+ def parent_records(parent, ids)
+ parent.issues.where(iid: ids.to_a)
+ end
+
+ def object_link_text_extras(issue, matches)
+ super + design_link_extras(issue, matches.named_captures['path'])
+ end
+
+ private
+
+ def issue_path(issue, project)
+ Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid)
+ end
+
+ def issue_url(issue, project)
+ Gitlab::Routing.url_helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue.iid)
+ end
+
+ def design_link_extras(issue, path)
+ if path == '/designs' && read_designs?(issue)
+ ['designs']
+ else
+ []
+ end
+ end
+
+ def read_designs?(issue)
+ issue.project.design_management_enabled?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/iteration_reference_filter.rb b/lib/banzai/filter/references/iteration_reference_filter.rb
new file mode 100644
index 00000000000..cf3d446147f
--- /dev/null
+++ b/lib/banzai/filter/references/iteration_reference_filter.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # The actual filter is implemented in the EE mixin
+ class IterationReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :iteration
+
+ def self.object_class
+ Iteration
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::IterationReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::IterationReferenceFilter')
diff --git a/lib/banzai/filter/references/label_reference_filter.rb b/lib/banzai/filter/references/label_reference_filter.rb
new file mode 100644
index 00000000000..8508aa2b3c3
--- /dev/null
+++ b/lib/banzai/filter/references/label_reference_filter.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces label references with links.
+ class LabelReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :label
+
+ def self.object_class
+ Label
+ end
+
+ def find_object(parent_object, id)
+ find_labels(parent_object).find(id)
+ end
+
+ def references_in(text, pattern = Label.reference_pattern)
+ labels = {}
+ unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
+ namespace, project = $~[:namespace], $~[:project]
+ project_path = full_project_path(namespace, project)
+ label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
+
+ if label
+ labels[label.id] = yield match, label.id, project, namespace, $~
+ "#{REFERENCE_PLACEHOLDER}#{label.id}"
+ else
+ match
+ end
+ end
+
+ return text if labels.empty?
+
+ escape_with_placeholders(unescaped_html, labels)
+ end
+
+ def find_label_cached(parent_ref, label_id, label_name)
+ cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do
+ find_label(parent_ref, label_id, label_name)
+ end
+ end
+
+ def find_label(parent_ref, label_id, label_name)
+ parent = parent_from_ref(parent_ref)
+ return unless parent
+
+ label_params = label_params(label_id, label_name)
+ find_labels(parent).find_by(label_params)
+ end
+
+ def find_labels(parent)
+ params = if parent.is_a?(Group)
+ { group_id: parent.id,
+ include_ancestor_groups: true,
+ only_group_labels: true }
+ else
+ { project: parent,
+ include_ancestor_groups: true }
+ end
+
+ LabelsFinder.new(nil, params).execute(skip_authorization: true)
+ end
+
+ # Parameters to pass to `Label.find_by` based on the given arguments
+ #
+ # id - Integer ID to pass. If present, returns {id: id}
+ # name - String name to pass. If `id` is absent, finds by name without
+ # surrounding quotes.
+ #
+ # Returns a Hash.
+ def label_params(id, name)
+ if name
+ { name: name.tr('"', '') }
+ else
+ { id: id.to_i }
+ end
+ end
+
+ def url_for_object(label, parent)
+ label_url_method =
+ if context[:label_url_method]
+ context[:label_url_method]
+ elsif parent.is_a?(Project)
+ :project_issues_url
+ end
+
+ return unless label_url_method
+
+ Gitlab::Routing.url_helpers.public_send(label_url_method, parent, label_name: label.name, only_path: context[:only_path]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def object_link_text(object, matches)
+ label_suffix = ''
+ parent = project || group
+
+ if project || full_path_ref?(matches)
+ project_path = full_project_path(matches[:namespace], matches[:project])
+ parent_from_ref = from_ref_cached(project_path)
+ reference = parent_from_ref.to_human_reference(parent)
+
+ label_suffix = " <i>in #{ERB::Util.html_escape(reference)}</i>" if reference.present?
+ end
+
+ presenter = object.present(issuable_subject: parent)
+ LabelsHelper.render_colored_label(presenter, suffix: label_suffix)
+ end
+
+ def wrap_link(link, label)
+ presenter = label.present(issuable_subject: project || group)
+ LabelsHelper.wrap_label_html(link, small: true, label: presenter)
+ end
+
+ def full_path_ref?(matches)
+ matches[:namespace] && matches[:project]
+ end
+
+ def reference_class(type, tooltip: true)
+ super + ' gl-link gl-label-link'
+ end
+
+ def object_link_title(object, matches)
+ presenter = object.present(issuable_subject: project || group)
+ LabelsHelper.label_tooltip_title(presenter)
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::LabelReferenceFilter')
diff --git a/lib/banzai/filter/references/merge_request_reference_filter.rb b/lib/banzai/filter/references/merge_request_reference_filter.rb
new file mode 100644
index 00000000000..872c33f6873
--- /dev/null
+++ b/lib/banzai/filter/references/merge_request_reference_filter.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces merge request references with links. References
+ # to merge requests that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class MergeRequestReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :merge_request
+
+ def self.object_class
+ MergeRequest
+ end
+
+ def url_for_object(mr, project)
+ h = Gitlab::Routing.url_helpers
+ h.project_merge_request_url(project, mr,
+ only_path: context[:only_path])
+ end
+
+ def object_link_title(object, matches)
+ # The method will return `nil` if object is not a commit
+ # allowing for properly handling the extended MR Tooltip
+ object_link_commit_title(object, matches)
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = super
+
+ if commit_ref = object_link_commit_ref(object, matches)
+ klass = reference_class(:commit, tooltip: false)
+ commit_ref_tag = %(<span class="#{klass}">#{commit_ref}</span>)
+
+ return extras.unshift(commit_ref_tag)
+ end
+
+ path = matches[:path] if matches.names.include?("path")
+
+ case path
+ when '/diffs'
+ extras.unshift "diffs"
+ when '/commits'
+ extras.unshift "commits"
+ when '/builds'
+ extras.unshift "builds"
+ end
+
+ extras
+ end
+
+ def parent_records(parent, ids)
+ parent.merge_requests
+ .where(iid: ids.to_a)
+ .includes(target_project: :namespace)
+ end
+
+ def reference_class(object_sym, options = {})
+ super(object_sym, tooltip: false)
+ end
+
+ def data_attributes_for(text, parent, object, **data)
+ super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
+ end
+
+ private
+
+ def object_link_commit_title(object, matches)
+ object_link_commit(object, matches)&.title
+ end
+
+ def object_link_commit_ref(object, matches)
+ object_link_commit(object, matches)&.short_id
+ end
+
+ def object_link_commit(object, matches)
+ return unless matches.names.include?('query') && query = matches[:query]
+
+ # Removes leading "?". CGI.parse expects "arg1&arg2&arg3"
+ params = CGI.parse(query.sub(/^\?/, ''))
+
+ return unless commit_sha = params['commit_id']&.first
+
+ if commit = find_commit_by_sha(object, commit_sha)
+ Commit.from_hash(commit.to_hash, object.project)
+ end
+ end
+
+ def find_commit_by_sha(object, commit_sha)
+ @all_commits ||= {}
+ @all_commits[object.id] ||= object.all_commits
+
+ @all_commits[object.id].find { |commit| commit.sha == commit_sha }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/milestone_reference_filter.rb b/lib/banzai/filter/references/milestone_reference_filter.rb
new file mode 100644
index 00000000000..49110194ddc
--- /dev/null
+++ b/lib/banzai/filter/references/milestone_reference_filter.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces milestone references with links.
+ class MilestoneReferenceFilter < AbstractReferenceFilter
+ include Gitlab::Utils::StrongMemoize
+
+ self.reference_type = :milestone
+
+ def self.object_class
+ Milestone
+ end
+
+ # Links to project milestones contain the IID, but when we're handling
+ # 'regular' references, we need to use the global ID to disambiguate
+ # between group and project milestones.
+ def find_object(parent, id)
+ return unless valid_context?(parent)
+
+ find_milestone_with_finder(parent, id: id)
+ end
+
+ def find_object_from_link(parent, iid)
+ return unless valid_context?(parent)
+
+ find_milestone_with_finder(parent, iid: iid)
+ end
+
+ def valid_context?(parent)
+ strong_memoize(:valid_context) do
+ group_context?(parent) || project_context?(parent)
+ end
+ end
+
+ def group_context?(parent)
+ strong_memoize(:group_context) do
+ parent.is_a?(Group)
+ end
+ end
+
+ def project_context?(parent)
+ strong_memoize(:project_context) do
+ parent.is_a?(Project)
+ end
+ end
+
+ def references_in(text, pattern = Milestone.reference_pattern)
+ # We'll handle here the references that follow the `reference_pattern`.
+ # Other patterns (for example, the link pattern) are handled by the
+ # default implementation.
+ return super(text, pattern) if pattern != Milestone.reference_pattern
+
+ milestones = {}
+ unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
+ milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
+
+ if milestone
+ milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
+ "#{REFERENCE_PLACEHOLDER}#{milestone.id}"
+ else
+ match
+ end
+ end
+
+ return text if milestones.empty?
+
+ escape_with_placeholders(unescaped_html, milestones)
+ end
+
+ def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
+ project_path = full_project_path(namespace_ref, project_ref)
+
+ # Returns group if project is not found by path
+ parent = parent_from_ref(project_path)
+
+ return unless parent
+
+ milestone_params = milestone_params(milestone_id, milestone_name)
+
+ find_milestone_with_finder(parent, milestone_params)
+ end
+
+ def milestone_params(iid, name)
+ if name
+ { name: name.tr('"', '') }
+ else
+ { iid: iid.to_i }
+ end
+ end
+
+ def find_milestone_with_finder(parent, params)
+ finder_params = milestone_finder_params(parent, params[:iid].present?)
+
+ MilestonesFinder.new(finder_params).find_by(params)
+ end
+
+ def milestone_finder_params(parent, find_by_iid)
+ { order: nil, state: 'all' }.tap do |params|
+ params[:project_ids] = parent.id if project_context?(parent)
+
+ # We don't support IID lookups because IIDs can clash between
+ # group/project milestones and group/subgroup milestones.
+ params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid
+ end
+ end
+
+ def self_and_ancestors_ids(parent)
+ if group_context?(parent)
+ parent.self_and_ancestors.select(:id)
+ elsif project_context?(parent)
+ parent.group&.self_and_ancestors&.select(:id)
+ end
+ end
+
+ def url_for_object(milestone, project)
+ Gitlab::Routing
+ .url_helpers
+ .milestone_url(milestone, only_path: context[:only_path])
+ end
+
+ def object_link_text(object, matches)
+ milestone_link = escape_once(super)
+ reference = object.project&.to_reference_base(project)
+
+ if reference.present?
+ "#{milestone_link} <i>in #{reference}</i>".html_safe
+ else
+ milestone_link
+ end
+ end
+
+ def object_link_title(object, matches)
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/project_reference_filter.rb b/lib/banzai/filter/references/project_reference_filter.rb
new file mode 100644
index 00000000000..522c6e0f5f3
--- /dev/null
+++ b/lib/banzai/filter/references/project_reference_filter.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces project references with links.
+ class ProjectReferenceFilter < ReferenceFilter
+ self.reference_type = :project
+
+ # Public: Find `namespace/project>` project references in text
+ #
+ # ProjectReferenceFilter.references_in(text) do |match, project|
+ # "<a href=...>#{project}></a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, and the String project name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(Project.markdown_reference_pattern) do |match|
+ yield match, "#{$~[:namespace]}/#{$~[:project]}"
+ end
+ end
+
+ def call
+ ref_pattern = Project.markdown_reference_pattern
+ ref_pattern_start = /\A#{ref_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ project_link_filter(content)
+ end
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_pattern_start
+ replace_link_node_with_href(node, index, link) do
+ project_link_filter(link, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Replace `namespace/project>` project references in text with links to the referenced
+ # project page.
+ #
+ # text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
+ #
+ # Returns a String with `namespace/project>` references replaced with links. All links
+ # have `gfm` and `gfm-project` class names attached for styling.
+ def project_link_filter(text, link_content: nil)
+ self.class.references_in(text) do |match, project_path|
+ cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
+ if project = projects_hash[project_path.downcase]
+ link_to_project(project, link_content: link_content) || match
+ else
+ match
+ end
+ end
+ end
+ end
+
+ # Returns a Hash containing all Project objects for the project
+ # references in the current document.
+ #
+ # The keys of this Hash are the project paths, the values the
+ # corresponding Project objects.
+ def projects_hash
+ @projects ||= Project.eager_load(:route, namespace: [:route])
+ .where_full_path_in(projects)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
+ end
+
+ # Returns all projects referenced in the current document.
+ def projects
+ refs = Set.new
+
+ nodes.each do |node|
+ node.to_html.scan(Project.markdown_reference_pattern) do
+ refs << "#{$~[:namespace]}/#{$~[:project]}"
+ end
+ end
+
+ refs.to_a
+ end
+
+ private
+
+ def urls
+ Gitlab::Routing.url_helpers
+ end
+
+ def link_class
+ reference_class(:project)
+ end
+
+ def link_to_project(project, link_content: nil)
+ url = urls.project_url(project, only_path: context[:only_path])
+ data = data_attribute(project: project.id)
+ content = link_content || project.to_reference
+
+ link_tag(url, data, content, project.name)
+ end
+
+ def link_tag(url, data, link_content, title)
+ %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb
new file mode 100644
index 00000000000..dd15c43f5d8
--- /dev/null
+++ b/lib/banzai/filter/references/reference_filter.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js
+module Banzai
+ module Filter
+ module References
+ # Base class for GitLab Flavored Markdown reference filters.
+ #
+ # References within <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # Context options:
+ # :project (required) - Current project, ignored if reference is cross-project.
+ # :only_path - Generate path-only links.
+ class ReferenceFilter < HTML::Pipeline::Filter
+ include RequestStoreReferenceCache
+ include OutputSafety
+
+ class << self
+ attr_accessor :reference_type
+
+ def call(doc, context = nil, result = nil)
+ new(doc, context, result).call_and_update_nodes
+ end
+ end
+
+ def initialize(doc, context = nil, result = nil)
+ super
+
+ @new_nodes = {}
+ @nodes = self.result[:reference_filter_nodes]
+ end
+
+ def call_and_update_nodes
+ with_update_nodes { call }
+ end
+
+ # Returns a data attribute String to attach to a reference link
+ #
+ # attributes - Hash, where the key becomes the data attribute name and the
+ # value is the data attribute value
+ #
+ # Examples:
+ #
+ # data_attribute(project: 1, issue: 2)
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ #
+ # data_attribute(project: 3, merge_request: 4)
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
+ #
+ # Returns a String
+ def data_attribute(attributes = {})
+ attributes = attributes.reject { |_, v| v.nil? }
+
+ attributes[:reference_type] ||= self.class.reference_type
+ attributes[:container] ||= 'body'
+ attributes[:placement] ||= 'top'
+ attributes.delete(:original) if context[:no_original_data]
+ attributes.map do |key, value|
+ %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
+ end.join(' ')
+ end
+
+ def ignore_ancestor_query
+ @ignore_ancestor_query ||= begin
+ parents = %w(pre code a style)
+ parents << 'blockquote' if context[:ignore_blockquotes]
+
+ parents.map { |n| "ancestor::#{n}" }.join(' or ')
+ end
+ end
+
+ def project
+ context[:project]
+ end
+
+ def group
+ context[:group]
+ end
+
+ def user
+ context[:user]
+ end
+
+ def skip_project_check?
+ context[:skip_project_check]
+ end
+
+ def reference_class(type, tooltip: true)
+ gfm_klass = "gfm gfm-#{type}"
+
+ return gfm_klass unless tooltip
+
+ "#{gfm_klass} has-tooltip"
+ end
+
+ # Ensure that a :project key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project unless skip_project_check?
+ end
+
+ # Iterates over all <a> and text() nodes in a document.
+ #
+ # Nodes are skipped whenever their ancestor is one of the nodes returned
+ # by `ignore_ancestor_query`. Link tags are not processed if they have a
+ # "gfm" class or the "href" attribute is empty.
+ def each_node
+ return to_enum(__method__) unless block_given?
+
+ doc.xpath(query).each do |node|
+ yield node
+ end
+ end
+
+ # Returns an Array containing all HTML nodes.
+ def nodes
+ @nodes ||= each_node.to_a
+ end
+
+ # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
+ def yield_valid_link(node)
+ link = unescape_link(node.attr('href').to_s)
+ inner_html = node.inner_html
+
+ return unless link.force_encoding('UTF-8').valid_encoding?
+
+ yield link, inner_html
+ end
+
+ def unescape_link(href)
+ CGI.unescape(href)
+ end
+
+ def replace_text_when_pattern_matches(node, index, pattern)
+ return unless node.text =~ pattern
+
+ content = node.to_html
+ html = yield content
+
+ replace_text_with_html(node, index, html) unless html == content
+ end
+
+ def replace_link_node_with_text(node, index)
+ html = yield
+
+ replace_text_with_html(node, index, html) unless html == node.text
+ end
+
+ def replace_link_node_with_href(node, index, link)
+ html = yield
+
+ replace_text_with_html(node, index, html) unless html == link
+ end
+
+ def text_node?(node)
+ node.is_a?(Nokogiri::XML::Text)
+ end
+
+ def element_node?(node)
+ node.is_a?(Nokogiri::XML::Element)
+ end
+
+ private
+
+ def query
+ @query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
+ | descendant-or-self::a[
+ not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
+ ]}
+ end
+
+ def replace_text_with_html(node, index, html)
+ replace_and_update_new_nodes(node, index, html)
+ end
+
+ def replace_and_update_new_nodes(node, index, html)
+ previous_node = node.previous
+ next_node = node.next
+ parent_node = node.parent
+ # Unfortunately node.replace(html) returns re-parented nodes, not the actual replaced nodes in the doc
+ # We need to find the actual nodes in the doc that were replaced
+ node.replace(html)
+ @new_nodes[index] = []
+
+ # We replaced node with new nodes, so we find first new node. If previous_node is nil, we take first parent child
+ new_node = previous_node ? previous_node.next : parent_node&.children&.first
+
+ # We iterate from first to last replaced node and store replaced nodes in @new_nodes
+ while new_node && new_node != next_node
+ @new_nodes[index] << new_node.xpath(query)
+ new_node = new_node.next
+ end
+
+ @new_nodes[index].flatten!
+ end
+
+ def only_path?
+ context[:only_path]
+ end
+
+ def with_update_nodes
+ @new_nodes = {}
+ yield.tap { update_nodes! }
+ end
+
+ # Once Filter completes replacing nodes, we update nodes with @new_nodes
+ def update_nodes!
+ @new_nodes.sort_by { |index, _new_nodes| -index }.each do |index, new_nodes|
+ nodes[index, 1] = new_nodes
+ end
+ result[:reference_filter_nodes] = nodes
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/snippet_reference_filter.rb b/lib/banzai/filter/references/snippet_reference_filter.rb
new file mode 100644
index 00000000000..bf7e0f78609
--- /dev/null
+++ b/lib/banzai/filter/references/snippet_reference_filter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces snippet references with links. References to
+ # snippets that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class SnippetReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :snippet
+
+ def self.object_class
+ Snippet
+ end
+
+ def find_object(project, id)
+ return unless project.is_a?(Project)
+
+ project.snippets.find_by(id: id)
+ end
+
+ def url_for_object(snippet, project)
+ h = Gitlab::Routing.url_helpers
+ h.project_snippet_url(project, snippet,
+ only_path: context[:only_path])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
new file mode 100644
index 00000000000..04665973f51
--- /dev/null
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces user or group references with links.
+ #
+ # A special `@all` reference is also supported.
+ class UserReferenceFilter < ReferenceFilter
+ self.reference_type = :user
+
+ # Public: Find `@user` user references in text
+ #
+ # UserReferenceFilter.references_in(text) do |match, username|
+ # "<a href=...>@#{user}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, and the String user name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(User.reference_pattern) do |match|
+ yield match, $~[:user]
+ end
+ end
+
+ def call
+ return doc if project.nil? && group.nil? && !skip_project_check?
+
+ ref_pattern = User.reference_pattern
+ ref_pattern_start = /\A#{ref_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ user_link_filter(content)
+ end
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_pattern_start
+ replace_link_node_with_href(node, index, link) do
+ user_link_filter(link, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Replace `@user` user references in text with links to the referenced
+ # user's profile page.
+ #
+ # text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
+ #
+ # Returns a String with `@user` references replaced with links. All links
+ # have `gfm` and `gfm-project_member` class names attached for styling.
+ def user_link_filter(text, link_content: nil)
+ self.class.references_in(text) do |match, username|
+ if username == 'all' && !skip_project_check?
+ link_to_all(link_content: link_content)
+ else
+ cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
+ if namespace = namespaces[username.downcase]
+ link_to_namespace(namespace, link_content: link_content) || match
+ else
+ match
+ end
+ end
+ end
+ end
+ end
+
+ # Returns a Hash containing all Namespace objects for the username
+ # references in the current document.
+ #
+ # The keys of this Hash are the namespace paths, the values the
+ # corresponding Namespace objects.
+ def namespaces
+ @namespaces ||= Namespace.eager_load(:owner, :route)
+ .where_full_path_in(usernames)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
+ end
+
+ # Returns all usernames referenced in the current document.
+ def usernames
+ refs = Set.new
+
+ nodes.each do |node|
+ node.to_html.scan(User.reference_pattern) do
+ refs << $~[:user]
+ end
+ end
+
+ refs.to_a
+ end
+
+ private
+
+ def urls
+ Gitlab::Routing.url_helpers
+ end
+
+ def link_class
+ [reference_class(:project_member, tooltip: false), "js-user-link"].join(" ")
+ end
+
+ def link_to_all(link_content: nil)
+ author = context[:author]
+
+ if author && !team_member?(author)
+ link_content
+ else
+ parent_url(link_content, author)
+ end
+ end
+
+ def link_to_namespace(namespace, link_content: nil)
+ if namespace.is_a?(Group)
+ link_to_group(namespace.full_path, namespace, link_content: link_content)
+ else
+ link_to_user(namespace.path, namespace, link_content: link_content)
+ end
+ end
+
+ def link_to_group(group, namespace, link_content: nil)
+ url = urls.group_url(group, only_path: context[:only_path])
+ data = data_attribute(group: namespace.id)
+ content = link_content || Group.reference_prefix + group
+
+ link_tag(url, data, content, namespace.full_name)
+ end
+
+ def link_to_user(user, namespace, link_content: nil)
+ url = urls.user_url(user, only_path: context[:only_path])
+ data = data_attribute(user: namespace.owner_id)
+ content = link_content || User.reference_prefix + user
+
+ link_tag(url, data, content, namespace.owner_name)
+ end
+
+ def link_tag(url, data, link_content, title)
+ %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
+ end
+
+ def parent
+ context[:project] || context[:group]
+ end
+
+ def parent_group?
+ parent.is_a?(Group)
+ end
+
+ def team_member?(user)
+ if parent_group?
+ parent.member?(user)
+ else
+ parent.team.member?(user)
+ end
+ end
+
+ def parent_url(link_content, author)
+ if parent_group?
+ url = urls.group_url(parent, only_path: context[:only_path])
+ data = data_attribute(group: group.id, author: author.try(:id))
+ else
+ url = urls.project_url(parent, only_path: context[:only_path])
+ data = data_attribute(project: project.id, author: author.try(:id))
+ end
+
+ content = link_content || User.reference_prefix + 'all'
+ link_tag(url, data, content, 'All Project and Group Members')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/vulnerability_reference_filter.rb b/lib/banzai/filter/references/vulnerability_reference_filter.rb
new file mode 100644
index 00000000000..e5f2408eda4
--- /dev/null
+++ b/lib/banzai/filter/references/vulnerability_reference_filter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # The actual filter is implemented in the EE mixin
+ class VulnerabilityReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :vulnerability
+
+ def self.object_class
+ Vulnerability
+ end
+
+ private
+
+ def project
+ context[:project]
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::VulnerabilityReferenceFilter')
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
deleted file mode 100644
index f4b6edb6174..00000000000
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces snippet references with links. References to
- # snippets that do not exist are ignored.
- #
- # This filter supports cross-project references.
- class SnippetReferenceFilter < AbstractReferenceFilter
- self.reference_type = :snippet
-
- def self.object_class
- Snippet
- end
-
- def find_object(project, id)
- return unless project.is_a?(Project)
-
- project.snippets.find_by(id: id)
- end
-
- def url_for_object(snippet, project)
- h = Gitlab::Routing.url_helpers
- h.project_snippet_url(project, snippet,
- only_path: context[:only_path])
- end
- end
- end
-end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
deleted file mode 100644
index 262385524f4..00000000000
--- a/lib/banzai/filter/user_reference_filter.rb
+++ /dev/null
@@ -1,180 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces user or group references with links.
- #
- # A special `@all` reference is also supported.
- class UserReferenceFilter < ReferenceFilter
- self.reference_type = :user
-
- # Public: Find `@user` user references in text
- #
- # UserReferenceFilter.references_in(text) do |match, username|
- # "<a href=...>@#{user}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, and the String user name.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(User.reference_pattern) do |match|
- yield match, $~[:user]
- end
- end
-
- def call
- return doc if project.nil? && group.nil? && !skip_project_check?
-
- ref_pattern = User.reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- user_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- user_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
- end
-
- # Replace `@user` user references in text with links to the referenced
- # user's profile page.
- #
- # text - String text to replace references in.
- # link_content - Original content of the link being replaced.
- #
- # Returns a String with `@user` references replaced with links. All links
- # have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, username|
- if username == 'all' && !skip_project_check?
- link_to_all(link_content: link_content)
- else
- cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
- if namespace = namespaces[username.downcase]
- link_to_namespace(namespace, link_content: link_content) || match
- else
- match
- end
- end
- end
- end
- end
-
- # Returns a Hash containing all Namespace objects for the username
- # references in the current document.
- #
- # The keys of this Hash are the namespace paths, the values the
- # corresponding Namespace objects.
- def namespaces
- @namespaces ||= Namespace.eager_load(:owner, :route)
- .where_full_path_in(usernames)
- .index_by(&:full_path)
- .transform_keys(&:downcase)
- end
-
- # Returns all usernames referenced in the current document.
- def usernames
- refs = Set.new
-
- nodes.each do |node|
- node.to_html.scan(User.reference_pattern) do
- refs << $~[:user]
- end
- end
-
- refs.to_a
- end
-
- private
-
- def urls
- Gitlab::Routing.url_helpers
- end
-
- def link_class
- [reference_class(:project_member, tooltip: false), "js-user-link"].join(" ")
- end
-
- def link_to_all(link_content: nil)
- author = context[:author]
-
- if author && !team_member?(author)
- link_content
- else
- parent_url(link_content, author)
- end
- end
-
- def link_to_namespace(namespace, link_content: nil)
- if namespace.is_a?(Group)
- link_to_group(namespace.full_path, namespace, link_content: link_content)
- else
- link_to_user(namespace.path, namespace, link_content: link_content)
- end
- end
-
- def link_to_group(group, namespace, link_content: nil)
- url = urls.group_url(group, only_path: context[:only_path])
- data = data_attribute(group: namespace.id)
- content = link_content || Group.reference_prefix + group
-
- link_tag(url, data, content, namespace.full_name)
- end
-
- def link_to_user(user, namespace, link_content: nil)
- url = urls.user_url(user, only_path: context[:only_path])
- data = data_attribute(user: namespace.owner_id)
- content = link_content || User.reference_prefix + user
-
- link_tag(url, data, content, namespace.owner_name)
- end
-
- def link_tag(url, data, link_content, title)
- %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
- end
-
- def parent
- context[:project] || context[:group]
- end
-
- def parent_group?
- parent.is_a?(Group)
- end
-
- def team_member?(user)
- if parent_group?
- parent.member?(user)
- else
- parent.team.member?(user)
- end
- end
-
- def parent_url(link_content, author)
- if parent_group?
- url = urls.group_url(parent, only_path: context[:only_path])
- data = data_attribute(group: group.id, author: author.try(:id))
- else
- url = urls.project_url(parent, only_path: context[:only_path])
- data = data_attribute(project: project.id, author: author.try(:id))
- end
-
- content = link_content || User.reference_prefix + 'all'
- link_tag(url, data, content, 'All Project and Group Members')
- end
- end
- end
-end
diff --git a/lib/banzai/filter/vulnerability_reference_filter.rb b/lib/banzai/filter/vulnerability_reference_filter.rb
deleted file mode 100644
index a59e9836d69..00000000000
--- a/lib/banzai/filter/vulnerability_reference_filter.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # The actual filter is implemented in the EE mixin
- class VulnerabilityReferenceFilter < IssuableReferenceFilter
- self.reference_type = :vulnerability
-
- def self.object_class
- Vulnerability
- end
-
- private
-
- def project
- context[:project]
- end
- end
- end
-end
-
-Banzai::Filter::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::VulnerabilityReferenceFilter')
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index e5ec0a0a006..028e3c44dc3 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -51,19 +51,19 @@ module Banzai
def self.reference_filters
[
- Filter::UserReferenceFilter,
- Filter::ProjectReferenceFilter,
- Filter::DesignReferenceFilter,
- Filter::IssueReferenceFilter,
- Filter::ExternalIssueReferenceFilter,
- Filter::MergeRequestReferenceFilter,
- Filter::SnippetReferenceFilter,
- Filter::CommitRangeReferenceFilter,
- Filter::CommitReferenceFilter,
- Filter::LabelReferenceFilter,
- Filter::MilestoneReferenceFilter,
- Filter::AlertReferenceFilter,
- Filter::FeatureFlagReferenceFilter
+ Filter::References::UserReferenceFilter,
+ Filter::References::ProjectReferenceFilter,
+ Filter::References::DesignReferenceFilter,
+ Filter::References::IssueReferenceFilter,
+ Filter::References::ExternalIssueReferenceFilter,
+ Filter::References::MergeRequestReferenceFilter,
+ Filter::References::SnippetReferenceFilter,
+ Filter::References::CommitRangeReferenceFilter,
+ Filter::References::CommitReferenceFilter,
+ Filter::References::LabelReferenceFilter,
+ Filter::References::MilestoneReferenceFilter,
+ Filter::References::AlertReferenceFilter,
+ Filter::References::FeatureFlagReferenceFilter
]
end
diff --git a/lib/banzai/pipeline/label_pipeline.rb b/lib/banzai/pipeline/label_pipeline.rb
index 725cccc4b2b..ccfda2052e6 100644
--- a/lib/banzai/pipeline/label_pipeline.rb
+++ b/lib/banzai/pipeline/label_pipeline.rb
@@ -6,7 +6,7 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::SanitizationFilter,
- Filter::LabelReferenceFilter
+ Filter::References::LabelReferenceFilter
]
end
end
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index 4bf98099662..65a5e28b704 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -17,15 +17,15 @@ module Banzai
def self.reference_filters
[
- Filter::UserReferenceFilter,
- Filter::IssueReferenceFilter,
- Filter::ExternalIssueReferenceFilter,
- Filter::MergeRequestReferenceFilter,
- Filter::SnippetReferenceFilter,
- Filter::CommitRangeReferenceFilter,
- Filter::CommitReferenceFilter,
- Filter::AlertReferenceFilter,
- Filter::FeatureFlagReferenceFilter
+ Filter::References::UserReferenceFilter,
+ Filter::References::IssueReferenceFilter,
+ Filter::References::ExternalIssueReferenceFilter,
+ Filter::References::MergeRequestReferenceFilter,
+ Filter::References::SnippetReferenceFilter,
+ Filter::References::CommitRangeReferenceFilter,
+ Filter::References::CommitReferenceFilter,
+ Filter::References::AlertReferenceFilter,
+ Filter::References::FeatureFlagReferenceFilter
]
end
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index a8610967708..12e182b38fc 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -56,10 +56,6 @@ module Gitlab
::Feature.enabled?(:codequality_mr_diff, project, default_enabled: false)
end
- def self.display_codequality_backend_comparison?(project)
- ::Feature.enabled?(:codequality_backend_comparison, project, default_enabled: :yaml)
- end
-
def self.multiple_cache_per_job?
::Feature.enabled?(:multiple_cache_per_job, default_enabled: :yaml)
end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
index 2def3a4d3a9..4402c42b136 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers.rb
@@ -6,6 +6,80 @@ module Gitlab
module ForeignKeyHelpers
include ::Gitlab::Database::SchemaHelpers
+ # Adds a foreign key with only minimal locking on the tables involved.
+ #
+ # In concept it works similarly to add_concurrent_foreign_key, but we have
+ # to add a special helper for partitioned tables for the following reasons:
+ # - add_concurrent_foreign_key sets the constraint to `NOT VALID`
+ # before validating it
+ # - Setting an FK to NOT VALID is not supported currently in Postgres (up to PG13)
+ # - Also, PostgreSQL will currently ignore NOT VALID constraints on partitions
+ # when adding a valid FK to the partitioned table, so they have to
+ # also be validated before we can add the final FK.
+ # Solution:
+ # - Add the foreign key first to each partition by using
+ # add_concurrent_foreign_key and validating it
+ # - Once all partitions have a foreign key, add it also to the partitioned
+ # table (there will be no need for a validation at that level)
+ # For those reasons, this method does not include an option to delay the
+ # validation, we have to force validate: true.
+ #
+ # source - The source (partitioned) table containing the foreign key.
+ # target - The target table the key points to.
+ # column - The name of the column to create the foreign key on.
+ # on_delete - The action to perform when associated data is removed,
+ # defaults to "CASCADE".
+ # name - The name of the foreign key.
+ #
+ def add_concurrent_partitioned_foreign_key(source, target, column:, on_delete: :cascade, name: nil)
+ partition_options = {
+ column: column,
+ on_delete: on_delete,
+
+ # We'll use the same FK name for all partitions and match it to
+ # the name used for the partitioned table to follow the convention
+ # used by PostgreSQL when adding FKs to new partitions
+ name: name.presence || concurrent_partitioned_foreign_key_name(source, column),
+
+ # Force the FK validation to true for partitions (and the partitioned table)
+ validate: true
+ }
+
+ if foreign_key_exists?(source, target, **partition_options)
+ warning_message = "Foreign key not created because it exists already " \
+ "(this may be due to an aborted migration or similar): " \
+ "source: #{source}, target: #{target}, column: #{partition_options[:column]}, "\
+ "name: #{partition_options[:name]}, on_delete: #{partition_options[:on_delete]}"
+
+ Gitlab::AppLogger.warn warning_message
+
+ return
+ end
+
+ partitioned_table = find_partitioned_table(source)
+
+ partitioned_table.postgres_partitions.order(:name).each do |partition|
+ add_concurrent_foreign_key(partition.identifier, target, **partition_options)
+ end
+
+ with_lock_retries do
+ add_foreign_key(source, target, **partition_options)
+ end
+ end
+
+ # Returns the name for a concurrent partitioned foreign key.
+ #
+ # Similar to concurrent_foreign_key_name (Gitlab::Database::MigrationHelpers)
+ # we just keep a separate method in case we want a different behavior
+ # for partitioned tables
+ #
+ def concurrent_partitioned_foreign_key_name(table, column, prefix: 'fk_rails_')
+ identifier = "#{table}_#{column}_fk"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
+
+ "#{prefix}#{hashed_identifier}"
+ end
+
# Creates a "foreign key" that references a partitioned table. Because foreign keys referencing partitioned
# tables are not supported in PG11, this does not create a true database foreign key, but instead implements the
# same functionality at the database level by using triggers.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 58ca3603ff0..ccbae0dbd34 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -925,9 +925,6 @@ msgstr ""
msgid "%{title} changes"
msgstr ""
-msgid "%{token}..."
-msgstr ""
-
msgid "%{totalCpu} (%{freeSpacePercentage}%{percentSymbol} free)"
msgstr ""
@@ -16124,9 +16121,6 @@ msgstr ""
msgid "If you remove this license, GitLab will fall back on the previous license, if any."
msgstr ""
-msgid "If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner."
-msgstr ""
-
msgid "If you want to re-enable two-factor authentication, visit %{two_factor_link}"
msgstr ""
@@ -18031,6 +18025,12 @@ msgstr ""
msgid "JiraService|This feature requires a Premium plan."
msgstr ""
+msgid "JiraService|This is a Premium feature"
+msgstr ""
+
+msgid "JiraService|This is an Ultimate feature"
+msgstr ""
+
msgid "JiraService|This issue is synchronized with Jira"
msgstr ""
@@ -18040,6 +18040,9 @@ msgstr ""
msgid "JiraService|Transition Jira issues to their final state:"
msgstr ""
+msgid "JiraService|Upgrade your plan to enable this feature of the Jira Integration."
+msgstr ""
+
msgid "JiraService|Use a password for server version and an API token for cloud version."
msgstr ""
@@ -22847,9 +22850,6 @@ msgstr ""
msgid "Part of merge request changes"
msgstr ""
-msgid "Partial token for reference only"
-msgstr ""
-
msgid "Participants"
msgstr ""
@@ -27223,9 +27223,6 @@ msgstr ""
msgid "Runners|Download latest binary"
msgstr ""
-msgid "Runners|Group"
-msgstr ""
-
msgid "Runners|IP Address"
msgstr ""
@@ -27271,16 +27268,22 @@ msgstr ""
msgid "Runners|Runner is paused, last contact was %{runner_contact} ago"
msgstr ""
-msgid "Runners|Shared"
+msgid "Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner."
msgstr ""
msgid "Runners|Show Runner installation instructions"
msgstr ""
-msgid "Runners|Specific"
+msgid "Runners|Tags"
msgstr ""
-msgid "Runners|Tags"
+msgid "Runners|This runner is associated with specific projects."
+msgstr ""
+
+msgid "Runners|This runner is available to all groups and projects in your GitLab instance."
+msgstr ""
+
+msgid "Runners|This runner is available to all projects and subgroups in a group."
msgstr ""
msgid "Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation."
@@ -27289,6 +27292,9 @@ msgstr ""
msgid "Runners|To install Runner in a container follow the instructions described in the GitLab documentation"
msgstr ""
+msgid "Runners|Use Group runners when you want all projects in a group to have access to a set of runners."
+msgstr ""
+
msgid "Runners|Value"
msgstr ""
@@ -27298,9 +27304,21 @@ msgstr ""
msgid "Runners|View installation instructions"
msgstr ""
+msgid "Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner."
+msgstr ""
+
msgid "Runners|You have used %{quotaUsed} out of %{quotaLimit} of your shared Runners pipeline minutes."
msgstr ""
+msgid "Runners|group"
+msgstr ""
+
+msgid "Runners|shared"
+msgstr ""
+
+msgid "Runners|specific"
+msgstr ""
+
msgid "Running"
msgstr ""
@@ -31950,9 +31968,6 @@ msgstr ""
msgid "This is a Jira user."
msgstr ""
-msgid "This is a Premium feature"
-msgstr ""
-
msgid "This is a confidential %{noteableTypeText}."
msgstr ""
@@ -32193,15 +32208,6 @@ msgstr ""
msgid "This repository was last checked %{last_check_timestamp}. The check passed."
msgstr ""
-msgid "This runner processes jobs for all projects in its group and subgroups."
-msgstr ""
-
-msgid "This runner processes jobs for all unassigned projects."
-msgstr ""
-
-msgid "This runner processes jobs for assigned projects only."
-msgstr ""
-
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""
@@ -33657,9 +33663,6 @@ msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
msgstr ""
-msgid "Upgrade your plan to enable this feature of the Jira Integration."
-msgstr ""
-
msgid "Upgrade your plan to improve merge requests."
msgstr ""
@@ -35793,9 +35796,6 @@ msgstr ""
msgid "You cannot impersonate an internal user"
msgstr ""
-msgid "You cannot make this a shared runner."
-msgstr ""
-
msgid "You cannot play this scheduled pipeline at the moment. Please wait a minute."
msgstr ""
@@ -37741,9 +37741,6 @@ msgstr ""
msgid "revised"
msgstr ""
-msgid "runners"
-msgstr ""
-
msgid "satisfied"
msgstr ""
diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping
index 5bc4b23f99f..e18a14e17d6 100755
--- a/scripts/verify-tff-mapping
+++ b/scripts/verify-tff-mapping
@@ -8,7 +8,7 @@ require 'set'
# The verification depend on the presence of actual test files,
# so they would fail if one of the test files mentioned here is deleted.
# To minimize the chance of this test failing due to unrelated changes,
-# the test files are chosen to be critical files that are unlikely to be deleted in a typical Merge Request
+# the test files are chosen to be critical files that are unlikely to be deleted in a typical merge request
tests = [
{
explanation: 'EE code should map to respective spec',
diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
index d2e0def6d0f..c78b838d0df 100644
--- a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
+++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
@@ -45,17 +45,6 @@ RSpec.describe Projects::DesignManagement::Designs::RawImagesController do
expect(response).to have_gitlab_http_status(:ok)
end
- context 'when the feature flag attachment_with_filename is disabled' do
- it 'serves files with just `attachment` in the disposition header' do
- stub_feature_flags(attachment_with_filename: false)
-
- subject
-
- expect(response.header['Content-Disposition']).to eq('attachment')
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
it 'serves files with Workhorse' do
subject
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 0b5894c0cfa..908d5741709 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -85,8 +85,7 @@ RSpec.describe 'Database schema' do
users: %w[color_scheme_id created_by_id theme_id email_opted_in_source_id],
users_star_projects: %w[user_id],
vulnerability_identifiers: %w[external_id],
- vulnerability_scanners: %w[external_id],
- web_hook_logs_part_0c5294f417: %w[web_hook_id]
+ vulnerability_scanners: %w[external_id]
}.with_indifferent_access.freeze
context 'for table' do
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 85c4aecfdc0..acfb7c2602a 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -49,19 +49,19 @@ RSpec.describe 'Runners' do
visit project_runners_path(project)
within '.activated-specific-runners' do
- expect(page).to have_content('Pause')
+ expect(page).to have_link('Pause')
end
click_on 'Pause'
within '.activated-specific-runners' do
- expect(page).to have_content('Resume')
+ expect(page).to have_link('Resume')
end
click_on 'Resume'
within '.activated-specific-runners' do
- expect(page).to have_content('Pause')
+ expect(page).to have_link('Pause')
end
end
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index 3938e7c7c22..d08a1904e06 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,7 +1,7 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
+import JiraUpgradeCta from '~/integrations/edit/components/jira_upgrade_cta.vue';
import eventHub from '~/integrations/edit/event_hub';
describe('JiraIssuesFields', () => {
@@ -28,23 +28,46 @@ describe('JiraIssuesFields', () => {
}
});
- const findEnableCheckbox = () => wrapper.find(GlFormCheckbox);
- const findProjectKey = () => wrapper.find(GlFormInput);
- const expectedBannerText = 'This is a Premium feature';
+ const findEnableCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findProjectKey = () => wrapper.findComponent(GlFormInput);
+ const findJiraUpgradeCta = () => wrapper.findComponent(JiraUpgradeCta);
const findJiraForVulnerabilities = () => wrapper.find('[data-testid="jira-for-vulnerabilities"]');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
+ describe('jira issues call to action', () => {
+ it('shows the premium message', () => {
+ createComponent({
+ props: { showJiraIssuesIntegration: false },
+ });
+
+ expect(findJiraUpgradeCta().props()).toMatchObject({
+ showPremiumMessage: true,
+ showUltimateMessage: false,
+ });
+ });
+
+ it('shows the ultimate message', () => {
+ createComponent({
+ props: {
+ showJiraIssuesIntegration: true,
+ showJiraVulnerabilitiesIntegration: false,
+ },
+ });
+
+ expect(findJiraUpgradeCta().props()).toMatchObject({
+ showPremiumMessage: false,
+ showUltimateMessage: true,
+ });
+ });
+ });
+
describe('template', () => {
describe('upgrade banner for non-Premium user', () => {
beforeEach(() => {
createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
});
- it('shows upgrade banner', () => {
- expect(wrapper.text()).toContain(expectedBannerText);
- });
-
it('does not show checkbox and input field', () => {
expect(findEnableCheckbox().exists()).toBe(false);
expect(findProjectKey().exists()).toBe(false);
@@ -57,7 +80,7 @@ describe('JiraIssuesFields', () => {
});
it('does not show upgrade banner', () => {
- expect(wrapper.text()).not.toContain(expectedBannerText);
+ expect(findJiraUpgradeCta().exists()).toBe(false);
});
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
@@ -125,6 +148,14 @@ describe('JiraIssuesFields', () => {
},
);
+ it('passes down the correct show-full-feature property', async () => {
+ await setEnableCheckbox(true);
+ expect(findJiraForVulnerabilities().attributes('show-full-feature')).toBe('true');
+ wrapper.setProps({ showJiraVulnerabilitiesIntegration: false });
+ await wrapper.vm.$nextTick();
+ expect(findJiraForVulnerabilities().attributes('show-full-feature')).toBeUndefined();
+ });
+
it('passes down the correct initial-issue-type-id value when value is empty', async () => {
await setEnableCheckbox(true);
expect(findJiraForVulnerabilities().attributes('initial-issue-type-id')).toBeUndefined();
diff --git a/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js b/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js
new file mode 100644
index 00000000000..e49a1619627
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js
@@ -0,0 +1,30 @@
+import { shallowMount } from '@vue/test-utils';
+import JiraUpgradeCta from '~/integrations/edit/components/jira_upgrade_cta.vue';
+
+describe('JiraUpgradeCta', () => {
+ let wrapper;
+
+ const contentMessage = 'Upgrade your plan to enable this feature of the Jira Integration.';
+
+ const createComponent = (propsData) => {
+ wrapper = shallowMount(JiraUpgradeCta, {
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('displays the correct message for premium and lower users', () => {
+ createComponent({ showPremiumMessage: true });
+ expect(wrapper.html()).toContain('This is a Premium feature');
+ expect(wrapper.html()).toContain(contentMessage);
+ });
+
+ it('displays the correct message for ultimate and lower users', () => {
+ createComponent({ showUltimateMessage: true });
+ expect(wrapper.html()).toContain('This is an Ultimate feature');
+ expect(wrapper.html()).toContain(contentMessage);
+ });
+});
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 708644cb7ee..9688cb47799 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -17,15 +17,14 @@ describe('CommitFormModal', () => {
let store;
let axiosMock;
- const createComponent = (method, state = {}, provide = {}) => {
+ const createComponent = (method, state = {}, provide = {}, propsData = {}) => {
store = createStore({ ...mockData.mockModal, ...state });
wrapper = extendedWrapper(
method(CommitFormModal, {
provide: {
...provide,
- glFeatures: { pickIntoProject: true },
},
- propsData: { ...mockData.modalPropsData },
+ propsData: { ...mockData.modalPropsData, ...propsData },
store,
attrs: {
static: true,
@@ -160,6 +159,12 @@ describe('CommitFormModal', () => {
});
it('Changes the target_project_id input value', async () => {
+ createComponent(
+ shallowMount,
+ {},
+ { glFeatures: { pickIntoProject: true } },
+ { isCherryPick: true },
+ );
findProjectsDropdown().vm.$emit('selectProject', '_changed_project_value_');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 184a1e458b5..87fe8619f28 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui';
+import { GlSkeletonLoader, GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
@@ -52,7 +52,7 @@ describe('User Popover Component', () => {
};
describe('when user is loading', () => {
- it('displays skeleton loaders', () => {
+ it('displays skeleton loader', () => {
createWrapper({
user: {
name: null,
@@ -65,7 +65,7 @@ describe('User Popover Component', () => {
},
});
- expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(4);
+ expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true);
});
});
diff --git a/spec/lib/api/entities/clusters/agent_spec.rb b/spec/lib/api/entities/clusters/agent_spec.rb
new file mode 100644
index 00000000000..04f7ec28407
--- /dev/null
+++ b/spec/lib/api/entities/clusters/agent_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::Clusters::Agent do
+ let_it_be(:cluster_agent) { create(:cluster_agent) }
+
+ subject { described_class.new(cluster_agent).as_json }
+
+ it 'includes basic fields' do
+ expect(subject).to include(
+ id: cluster_agent.id,
+ config_project: a_hash_including(id: cluster_agent.project_id)
+ )
+ end
+end
diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
index 797f1c8d52f..076c112ac87 100644
--- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AbstractReferenceFilter do
+RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
let_it_be(:project) { create(:project) }
let(:doc) { Nokogiri::HTML.fragment('') }
diff --git a/spec/lib/banzai/filter/alert_reference_filter_spec.rb b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
index c57a8a7321c..7c6b0cac24b 100644
--- a/spec/lib/banzai/filter/alert_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::AlertReferenceFilter do
+RSpec.describe Banzai::Filter::References::AlertReferenceFilter do
include FilterSpecHelper
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb
index f04d3212437..b235de06b30 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/commit_range_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::CommitRangeReferenceFilter do
+RSpec.describe Banzai::Filter::References::CommitRangeReferenceFilter do
include FilterSpecHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
index 925fd031d95..bee8e42d12e 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::CommitReferenceFilter do
+RSpec.describe Banzai::Filter::References::CommitReferenceFilter do
include FilterSpecHelper
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/lib/banzai/filter/design_reference_filter_spec.rb b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb
index 847c398964a..52514ad17fc 100644
--- a/spec/lib/banzai/filter/design_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/design_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::DesignReferenceFilter do
+RSpec.describe Banzai::Filter::References::DesignReferenceFilter do
include FilterSpecHelper
include DesignManagementTestHelpers
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
index 45c55bdbeff..3b274f98020 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/external_issue_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do
+RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter do
include FilterSpecHelper
let_it_be_with_refind(:project) { create(:project) }
diff --git a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb b/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb
index 2d7089853cf..c64b66f746e 100644
--- a/spec/lib/banzai/filter/feature_flag_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/feature_flag_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do
+RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter do
include FilterSpecHelper
let_it_be(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
index 4b8b575c1f0..b849355f6db 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::IssueReferenceFilter do
+RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
include FilterSpecHelper
include DesignManagementTestHelpers
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
index 726ef8c57ab..db7dda96cad 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'html/pipeline'
-RSpec.describe Banzai::Filter::LabelReferenceFilter do
+RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
include FilterSpecHelper
let(:project) { create(:project, :public, name: 'sample-project') }
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
index 811c2aca342..7a634b0b513 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/merge_request_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::MergeRequestReferenceFilter do
+RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
include FilterSpecHelper
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
index 276fa7952be..dafdc71ce64 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::MilestoneReferenceFilter do
+RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do
include FilterSpecHelper
let_it_be(:parent_group) { create(:group, :public) }
diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
index ac7a90a5893..7a77d57cd42 100644
--- a/spec/lib/banzai/filter/project_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::ProjectReferenceFilter do
+RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
include FilterSpecHelper
def invalidate_reference(reference)
diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/references/reference_filter_spec.rb
index 2888965dbc4..4bcb41ef2a9 100644
--- a/spec/lib/banzai/filter/reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::ReferenceFilter do
+RSpec.describe Banzai::Filter::References::ReferenceFilter do
let(:project) { build_stubbed(:project) }
describe '#each_node' do
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
index f23fbc5be88..32a706925ba 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::SnippetReferenceFilter do
+RSpec.describe Banzai::Filter::References::SnippetReferenceFilter do
include FilterSpecHelper
let(:project) { create(:project, :public) }
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
index b8baccf6658..e4703606b47 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::UserReferenceFilter do
+RSpec.describe Banzai::Filter::References::UserReferenceFilter do
include FilterSpecHelper
def get_reference(user)
diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
index 6eabf1db6f5..e24177a7043 100644
--- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do
issue = create(:issue, project: project)
markdown = "text #{issue.to_reference(project, full: true)}"
- expect_any_instance_of(Banzai::Filter::ReferenceFilter).to receive(:each_node).once
+ expect_any_instance_of(Banzai::Filter::References::ReferenceFilter).to receive(:each_node).once
described_class.call(markdown, project: project)
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 9d9124b8d0b..4b46c98117f 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2053,14 +2053,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) }
it { is_expected.to be_truthy }
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(codequality_backend_comparison: false)
- end
-
- it { is_expected.to be_falsey }
- end
end
context 'when head pipeline does not have a codequality report' do
@@ -3887,17 +3879,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'when service class is Ci::CompareCodequalityReportsService' do
let(:service_class) { 'Ci::CompareCodequalityReportsService' }
- context 'when feature flag is enabled' do
- it { is_expected.to be_truthy }
- end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(codequality_backend_comparison: false)
- end
-
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to be_truthy }
end
context 'when service class is different' do
diff --git a/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb b/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb
new file mode 100644
index 00000000000..bc1815558d3
--- /dev/null
+++ b/spec/models/sidebars/projects/menus/learn_gitlab/menu_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::LearnGitlab::Menu do
+ let(:project) { build(:project) }
+ let(:experiment_enabled) { true }
+ let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project, learn_gitlab_experiment_enabled: experiment_enabled) }
+
+ subject { described_class.new(context) }
+
+ it 'does not contain any sub menu' do
+ expect(subject.instance_variable_get(:@items)).to be_empty
+ end
+
+ describe '#render?' do
+ context 'when learn gitlab experiment is enabled' do
+ it 'returns true' do
+ expect(subject.render?).to eq true
+ end
+ end
+
+ context 'when learn gitlab experiment is disabled' do
+ let(:experiment_enabled) { false }
+
+ it 'returns false' do
+ expect(subject.render?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 468e0066362..3abf2a651a0 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1803,8 +1803,8 @@ RSpec.describe User do
it 'aborts all running pipelines and related jobs' do
expect(user).to receive(:pipelines).and_return(pipelines)
- expect(Ci::AbortPipelinesService).to receive(:new).and_return(service)
- expect(service).to receive(:execute).with(pipelines, :user_blocked)
+ expect(Ci::DropPipelineService).to receive(:new).and_return(service)
+ expect(service).to receive(:execute_async_for_all).with(pipelines, :user_blocked, user)
user.block
end
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 03412fe1b66..cf0d8a632f1 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -143,7 +143,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when there is a pending job' do
let(:expected_job_info) do
- { 'name' => job.name,
+ { 'id' => job.id,
+ 'name' => job.name,
'stage' => job.stage,
'project_id' => job.project.id,
'project_name' => job.project.name }
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 4f73917763f..cff006bed94 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -100,6 +100,18 @@ RSpec.describe API::Jobs do
end
end
+ context 'when token is valid but not CI_JOB_TOKEN' do
+ let(:token) { create(:personal_access_token, user: user) }
+
+ include_context 'with auth headers' do
+ let(:header) { { 'Private-Token' => token.token } }
+ end
+
+ it 'returns not found' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'with job token authentication header' do
include_context 'with auth headers' do
let(:header) { { API::Helpers::Runner::JOB_TOKEN_HEADER => running_job.token } }
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 1eba9ae4e5e..985e18f27a0 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -3,8 +3,10 @@
require 'spec_helper'
RSpec.describe EnvironmentSerializer do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ include CreateEnvironmentsHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project, :repository) }
let(:json) do
described_class
@@ -12,43 +14,18 @@ RSpec.describe EnvironmentSerializer do
.represent(resource)
end
- before do
+ before_all do
project.add_developer(user)
end
- context 'when there is a single object provided' do
- let(:project) { create(:project, :repository) }
- let(:deployable) { create(:ci_build) }
- let(:deployment) do
- create(:deployment, :success,
- deployable: deployable,
- user: user,
- project: project,
- sha: project.commit.id)
- end
-
- let(:resource) { deployment.environment }
-
- before do
- create(:ci_build, :manual, name: 'manual1', pipeline: deployable.pipeline)
- end
-
- it 'contains important elements of environment' do
- expect(json)
- .to include(:name, :external_url, :environment_path, :last_deployment)
- end
+ it_behaves_like 'avoid N+1 on environments serialization'
- it 'contains relevant information about last deployment' do
- last_deployment = json.fetch(:last_deployment)
+ context 'when there is a collection of objects provided' do
+ let(:resource) { project.environments }
- expect(last_deployment)
- .to include(:ref, :user, :commit, :deployable, :manual_actions)
+ before_all do
+ create_list(:environment, 2, project: project)
end
- end
-
- context 'when there is a collection of objects provided' do
- let(:project) { create(:project) }
- let(:resource) { create_list(:environment, 2) }
it 'contains important elements of environment' do
expect(json.first)
@@ -207,4 +184,11 @@ RSpec.describe EnvironmentSerializer do
end
end
end
+
+ def create_environment_with_associations(project)
+ create(:environment, project: project).tap do |environment|
+ create(:deployment, :success, environment: environment, project: project)
+ create(:deployment, :running, environment: environment, project: project)
+ end
+ end
end
diff --git a/spec/services/ci/drop_pipeline_service_spec.rb b/spec/services/ci/drop_pipeline_service_spec.rb
new file mode 100644
index 00000000000..4adbb99b9e2
--- /dev/null
+++ b/spec/services/ci/drop_pipeline_service_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::DropPipelineService do
+ let_it_be(:user) { create(:user) }
+
+ let(:failure_reason) { :user_blocked }
+
+ let!(:cancelable_pipeline) { create(:ci_pipeline, :running, user: user) }
+ let!(:running_build) { create(:ci_build, :running, pipeline: cancelable_pipeline) }
+ let!(:success_pipeline) { create(:ci_pipeline, :success, user: user) }
+ let!(:success_build) { create(:ci_build, :success, pipeline: success_pipeline) }
+
+ describe '#execute_async_for_all' do
+ subject { described_class.new.execute_async_for_all(user.pipelines, failure_reason, user) }
+
+ it 'drops only cancelable pipelines asynchronously', :sidekiq_inline do
+ subject
+
+ expect(cancelable_pipeline.reload).to be_failed
+ expect(running_build.reload).to be_failed
+
+ expect(success_pipeline.reload).to be_success
+ expect(success_build.reload).to be_success
+ end
+ end
+
+ describe '#execute' do
+ subject { described_class.new.execute(cancelable_pipeline.id, failure_reason) }
+
+ def drop_pipeline!(pipeline)
+ described_class.new.execute(pipeline, failure_reason)
+ end
+
+ it 'drops each cancelable build in the pipeline', :aggregate_failures do
+ drop_pipeline!(cancelable_pipeline)
+
+ expect(running_build.reload).to be_failed
+ expect(running_build.failure_reason).to eq(failure_reason.to_s)
+
+ expect(success_build.reload).to be_success
+ end
+
+ it 'avoids N+1 queries when reading data' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ drop_pipeline!(cancelable_pipeline)
+ end.count
+
+ writes_per_build = 2
+ expected_reads_count = control_count - writes_per_build
+
+ create_list(:ci_build, 5, :running, pipeline: cancelable_pipeline)
+
+ expect do
+ drop_pipeline!(cancelable_pipeline)
+ end.not_to exceed_query_limit(expected_reads_count + (5 * writes_per_build))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
index 62aaec85162..c939c306d93 100644
--- a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
@@ -36,16 +36,6 @@ RSpec.shared_examples 'raw snippet blob' do
expect(response.header['Content-Disposition']).to match "attachment; filename=\"#{filepath}\""
end
-
- context 'when the feature flag attachment_with_filename is disabled' do
- it 'returns just attachment in the disposition header' do
- stub_feature_flags(attachment_with_filename: false)
-
- subject
-
- expect(response.header['Content-Disposition']).to eq 'attachment'
- end
- end
end
end
diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
new file mode 100644
index 00000000000..00146335ef7
--- /dev/null
+++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+RSpec.shared_examples 'avoid N+1 on environments serialization' do
+ it 'avoids N+1 database queries with grouping', :request_store do
+ create_environment_with_associations(project)
+
+ control = ActiveRecord::QueryRecorder.new { serialize(grouping: true) }
+
+ create_environment_with_associations(project)
+
+ expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count)
+ end
+
+ it 'avoids N+1 database queries without grouping', :request_store do
+ create_environment_with_associations(project)
+
+ control = ActiveRecord::QueryRecorder.new { serialize(grouping: false) }
+
+ create_environment_with_associations(project)
+
+ expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count)
+ end
+
+ def serialize(grouping:)
+ EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer|
+ serializer.within_folders if grouping
+ serializer.represent(Environment.where(project: project))
+ end
+ end
+end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index b6aff316b17..f4a5fcc13bb 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -50,6 +50,16 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
+ describe 'Learn GitLab' do
+ it 'has a link to the learn GitLab experiment' do
+ allow(view).to receive(:learn_gitlab_experiment_enabled?).and_return(true)
+
+ render
+
+ expect(rendered).to have_link('Learn GitLab', href: project_learn_gitlab_path(project))
+ end
+ end
+
describe 'issue boards' do
it 'has board tab' do
render
diff --git a/spec/views/shared/runners/show.html.haml_spec.rb b/spec/views/shared/runners/show.html.haml_spec.rb
index 5e2812eb48a..91a6a31daae 100644
--- a/spec/views/shared/runners/show.html.haml_spec.rb
+++ b/spec/views/shared/runners/show.html.haml_spec.rb
@@ -34,19 +34,19 @@ RSpec.describe 'shared/runners/show.html.haml' do
describe 'Runner id and type' do
context 'when runner is of type instance' do
- it { is_expected.to have_content("Runner ##{runner.id} Shared") }
+ it { is_expected.to have_content("Runner ##{runner.id} shared") }
end
context 'when runner is of type group' do
let(:runner) { create(:ci_runner, :group) }
- it { is_expected.to have_content("Runner ##{runner.id} Group") }
+ it { is_expected.to have_content("Runner ##{runner.id} group") }
end
context 'when runner is of type project' do
let(:runner) { create(:ci_runner, :project) }
- it { is_expected.to have_content("Runner ##{runner.id} Specific") }
+ it { is_expected.to have_content("Runner ##{runner.id} specific") }
end
end
diff --git a/spec/workers/ci/drop_pipeline_worker_spec.rb b/spec/workers/ci/drop_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..5e626112520
--- /dev/null
+++ b/spec/workers/ci/drop_pipeline_worker_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::DropPipelineWorker do
+ include AfterNextHelpers
+
+ let(:pipeline) { create(:ci_pipeline, :running) }
+ let(:failure_reason) { :user_blocked }
+
+ describe '#perform' do
+ subject { described_class.new.perform(pipeline.id, failure_reason) }
+
+ it 'calls delegates to the service' do
+ expect_next(Ci::DropPipelineService).to receive(:execute).with(pipeline, failure_reason)
+
+ subject
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let!(:running_build) { create(:ci_build, :running, pipeline: pipeline) }
+ let!(:success_build) { create(:ci_build, :success, pipeline: pipeline) }
+
+ let(:job_args) { [pipeline.id, failure_reason] }
+
+ it 'executes the service', :aggregate_failures do
+ subject
+
+ expect(running_build.reload).to be_failed
+ expect(running_build.failure_reason).to eq(failure_reason.to_s)
+
+ expect(success_build.reload).to be_success
+ end
+ end
+ end
+end
diff --git a/workhorse/_support/changelog b/workhorse/_support/changelog
index 0e733cc0062..3b9efc21fcc 100755
--- a/workhorse/_support/changelog
+++ b/workhorse/_support/changelog
@@ -52,7 +52,7 @@ class ChangelogOptionParser
options.force = value
end
- opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
+ opts.on('-m', '--merge-request [integer]', Integer, 'Merge request ID') do |value|
options.merge_request = value
end