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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-07 21:09:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-07 21:09:45 +0300
commit413119517cca6a47f52d77b49ae3cab4cdaf9884 (patch)
tree046eb80cb92bb948cd49a99b7c34bf8dc6884c4f
parent40b78ea2b6f5f0ef730c2cd811911be3449562e6 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_manual_todo.yml31
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_instructions.js9
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue194
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue6
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue52
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql8
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql14
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue244
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue249
-rw-r--r--app/controllers/registrations/welcome_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/finders/repositories/branch_names_finder.rb24
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb11
-rw-r--r--app/helpers/avatars_helper.rb6
-rw-r--r--app/helpers/in_product_marketing_helper.rb3
-rw-r--r--app/mailers/emails/in_product_marketing.rb5
-rw-r--r--app/models/concerns/avatarable.rb1
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb241
-rw-r--r--app/models/concerns/ci/has_status.rb8
-rw-r--r--app/models/concerns/deprecated_assignee.rb2
-rw-r--r--app/models/namespace_setting.rb7
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_services/chat_message/merge_message.rb2
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/wiki.rb12
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml2
-rw-r--r--app/views/devise/shared/_email_opted_in.html.haml7
-rw-r--r--app/views/registrations/welcome/show.html.haml1
-rw-r--r--app/workers/namespaces/in_product_marketing_emails_worker.rb2
-rw-r--r--changelogs/unreleased/220647-add-prefix-to-CSS-class-of-syntax-highlighting-blocks.yml5
-rw-r--r--changelogs/unreleased/296888-resolve-conflicts-popover-does-not-show.yml5
-rw-r--r--changelogs/unreleased/324105-add-commit-email-to-users-api.yml5
-rw-r--r--changelogs/unreleased/324306-fj-enable-gitaly-find-file-feature-flag.yml5
-rw-r--r--changelogs/unreleased/326102-fe-cleanup-runner-instructions-project-group-parameters.yml6
-rw-r--r--changelogs/unreleased/92508-enable-not-filters-for-mr-labels-graphql.yml5
-rw-r--r--changelogs/unreleased/dblessing_cascading_settings_final.yml5
-rw-r--r--changelogs/unreleased/in-product-email-campaigns-self-managed.yml5
-rw-r--r--changelogs/unreleased/mc-backstage-use-redis-in-branch-finder.yml5
-rw-r--r--changelogs/unreleased/rails-save-bang-initializers.yml5
-rw-r--r--changelogs/unreleased/remove-avatar-cache-ff.yml5
-rw-r--r--changelogs/unreleased/sy-on-call-usage-ping.yml5
-rw-r--r--config/feature_flags/development/cascading_namespace_settings.yml8
-rw-r--r--config/feature_flags/development/gitaly_find_file.yml8
-rw-r--r--config/feature_flags/development/project_sidebar_refactor.yml (renamed from config/feature_flags/development/avatar_cache_for_email.yml)10
-rw-r--r--config/feature_flags/development/usage_data_i_incident_management_oncall_notification_sent.yml8
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--db/migrate/20210308175224_change_namespace_settings_delayed_project_removal_null.rb14
-rw-r--r--db/migrate/20210308175225_add_lock_delayed_project_removal_to_namespace_settings.rb9
-rw-r--r--db/migrate/20210308175226_add_delayed_project_removal_to_application_settings.rb9
-rw-r--r--db/migrate/20210308175227_add_lock_delayed_project_removal_to_application_settings.rb9
-rw-r--r--db/schema_migrations/202103081752241
-rw-r--r--db/schema_migrations/202103081752251
-rw-r--r--db/schema_migrations/202103081752261
-rw-r--r--db/schema_migrations/202103081752271
-rw-r--r--db/structure.sql5
-rw-r--r--doc/administration/operations/extra_sidekiq_processes.md2
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md24
-rw-r--r--doc/api/users.md5
-rw-r--r--doc/ci/environments/index.md141
-rw-r--r--doc/development/fe_guide/accessibility.md364
-rw-r--r--doc/development/permissions.md28
-rw-r--r--doc/development/usage_ping/dictionary.md24
-rw-r--r--doc/user/admin_area/merge_requests_approvals.md2
-rw-r--r--lib/api/entities/user_public.rb1
-rw-r--r--lib/banzai/filter/math_filter.rb2
-rw-r--r--lib/banzai/filter/suggestion_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb2
-rw-r--r--lib/gitlab/ci/config.rb6
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/process.rb1
-rw-r--r--lib/gitlab/diff/suggestions_parser.rb2
-rw-r--r--lib/gitlab/git/wiki.rb13
-rw-r--r--lib/gitlab/git/wiki_file.rb24
-rw-r--r--lib/gitlab/gitaly_client/attributes_bag.rb2
-rw-r--r--lib/gitlab/gitaly_client/wiki_file.rb11
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb26
-rw-r--r--lib/gitlab/graphql/negatable_arguments.rb53
-rw-r--r--lib/gitlab/repository_set_cache.rb15
-rw-r--r--lib/gitlab/usage_data_counters/known_events/common.yml6
-rw-r--r--locale/gitlab.pot71
-rw-r--r--package.json2
-rw-r--r--spec/benchmarks/banzai_benchmark.rb7
-rw-r--r--spec/controllers/registrations/welcome_controller_spec.rb22
-rw-r--r--spec/features/markdown/markdown_spec.rb11
-rw-r--r--spec/features/registrations/welcome_spec.rb21
-rw-r--r--spec/finders/repositories/branch_names_finder_spec.rb25
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/public.json3
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js36
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js28
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/mock_data.js16
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js184
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js138
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb19
-rw-r--r--spec/graphql/types/project_type_spec.rb1
-rw-r--r--spec/helpers/avatars_helper_spec.rb24
-rw-r--r--spec/helpers/markup_helper_spec.rb2
-rw-r--r--spec/initializers/active_record_locking_spec.rb4
-rw-r--r--spec/initializers/fog_google_https_private_urls_spec.rb2
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/suggestion_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb12
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb6
-rw-r--r--spec/lib/gitlab/graphql/negatable_arguments_spec.rb45
-rw-r--r--spec/lib/gitlab/profiler_spec.rb9
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb12
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
-rw-r--r--spec/mailers/emails/in_product_marketing_spec.rb34
-rw-r--r--spec/models/concerns/cascading_namespace_setting_attribute_spec.rb320
-rw-r--r--spec/models/concerns/ci/has_status_spec.rb6
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb26
-rw-r--r--spec/models/repository_spec.rb16
-rw-r--r--spec/models/user_spec.rb28
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb2
-rw-r--r--spec/spec_helper.rb18
-rw-r--r--spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb2
-rw-r--r--spec/views/registrations/welcome/show.html.haml_spec.rb24
-rw-r--r--spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb56
-rw-r--r--yarn.lock8
128 files changed, 2349 insertions, 1081 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 686c6fe8c1b..2201c563685 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -162,8 +162,6 @@ Rails/SaveBang:
- 'spec/graphql/mutations/merge_requests/set_locked_spec.rb'
- 'spec/graphql/mutations/merge_requests/set_wip_spec.rb'
- 'spec/graphql/resolvers/boards_resolver_spec.rb'
- - 'spec/initializers/active_record_locking_spec.rb'
- - 'spec/initializers/fog_google_https_private_urls_spec.rb'
- 'spec/lib/after_commit_queue_spec.rb'
- 'spec/lib/backup/manager_spec.rb'
- 'spec/lib/gitlab/alerting/alert_spec.rb'
@@ -544,29 +542,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- ee/spec/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate_spec.rb
- ee/spec/lib/gitlab/insights/project_insights_config_spec.rb
- ee/spec/lib/gitlab/sitemaps/url_extractor_spec.rb
- - ee/spec/models/analytics/cycle_analytics/group_level_spec.rb
- - ee/spec/models/burndown_spec.rb
- - ee/spec/models/ci/build_spec.rb
- - ee/spec/models/ci/daily_build_group_report_result_spec.rb
- - ee/spec/models/ci/minutes/notification_spec.rb
- - ee/spec/models/concerns/epic_tree_sorting_spec.rb
- - ee/spec/models/dora/daily_metrics_spec.rb
- - ee/spec/models/ee/ci/build_dependencies_spec.rb
- - ee/spec/models/ee/iteration_spec.rb
- - ee/spec/models/ee/namespace/root_storage_size_spec.rb
- - ee/spec/models/ee/namespace_spec.rb
- - ee/spec/models/ee/personal_access_token_spec.rb
- - ee/spec/models/epic_spec.rb
- - ee/spec/models/geo/lfs_object_registry_spec.rb
- - ee/spec/models/gitlab_subscription_spec.rb
- - ee/spec/models/group_member_spec.rb
- - ee/spec/models/instance_security_dashboard_spec.rb
- - ee/spec/models/issue_spec.rb
- - ee/spec/models/label_note_spec.rb
- - ee/spec/models/merge_request_spec.rb
- - ee/spec/models/project_spec.rb
- - ee/spec/models/requirements_management/test_report_spec.rb
- - ee/spec/models/vulnerabilities/finding_spec.rb
- ee/spec/policies/clusters/agent_token_policy_spec.rb
- ee/spec/policies/compliance_management/framework_policy_spec.rb
- ee/spec/policies/group_policy_spec.rb
@@ -631,9 +606,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- ee/spec/requests/api/wikis_spec.rb
- ee/spec/requests/callout_spec.rb
- ee/spec/requests/git_http_geo_spec.rb
- - ee/spec/requests/projects/issues_controller_spec.rb
- - ee/spec/requests/projects/on_demand_scans_controller_spec.rb
- - ee/spec/requests/projects/security/scanned_resources_controller_spec.rb
- ee/spec/requests/repositories/git_http_controller_spec.rb
- ee/spec/serializers/clusters/environment_serializer_spec.rb
- ee/spec/serializers/dependency_entity_spec.rb
@@ -700,11 +672,8 @@ RSpec/EmptyLineAfterFinalLetItBe:
- ee/spec/services/external_approval_rules/update_service_spec.rb
- ee/spec/services/gitlab_subscriptions/activate_service_spec.rb
- ee/spec/services/gitlab_subscriptions/apply_trial_service_spec.rb
- - ee/spec/services/ide/schemas_config_service_spec.rb
- ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb
- ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb
- - ee/spec/services/issues/build_service_spec.rb
- - ee/spec/services/issues/export_csv_service_spec.rb
- ee/spec/services/merge_request_approval_settings/update_service_spec.rb
- ee/spec/services/merge_trains/check_status_service_spec.rb
- ee/spec/services/merge_trains/create_pipeline_service_spec.rb
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
index 51028e585b8..e83c73edfde 100644
--- a/app/assets/javascripts/pages/shared/mount_runner_instructions.js
+++ b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import InstallRunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
Vue.use(VueApollo);
@@ -10,7 +10,6 @@ export function initInstallRunner(componentId = 'js-install-runner') {
if (installRunnerEl) {
const defaultClient = createDefaultClient();
- const { projectPath, groupPath } = installRunnerEl.dataset;
const apolloProvider = new VueApollo({
defaultClient,
@@ -20,12 +19,8 @@ export function initInstallRunner(componentId = 'js-install-runner') {
new Vue({
el: installRunnerEl,
apolloProvider,
- provide: {
- projectPath,
- groupPath,
- },
render(createElement) {
- return createElement(InstallRunnerInstructions);
+ return createElement(RunnerInstructions);
},
});
}
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
index 51a95612d3f..01baf0a42d5 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
@@ -10,6 +10,10 @@ export default {
type: String,
required: true,
},
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
isHighlighted: {
type: Boolean,
required: false,
@@ -32,6 +36,9 @@ export default {
},
},
computed: {
+ id() {
+ return `${this.jobName}-${this.pipelineId}`;
+ },
jobPillClasses() {
return [
{ 'gl-opacity-3': this.isFadedOut },
@@ -52,7 +59,7 @@ export default {
<template>
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
- :id="jobName"
+ :id="id"
class="gl-w-15 gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
:class="jobPillClasses"
@mouseover="onMouseEnter"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
index 707d6966e77..1f8c4a9aa8b 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -3,9 +3,7 @@ import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
-import { createJobsHash, generateJobNeedsDict } from '../../utils';
-import { generateLinksData } from '../graph_shared/drawing_utils';
-import { parseData } from '../parsing_utils';
+import LinksLayer from '../graph_shared/links_layer.vue';
import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
@@ -13,10 +11,12 @@ export default {
components: {
GlAlert,
JobPill,
+ LinksLayer,
StagePill,
},
CONTAINER_REF: 'PIPELINE_GRAPH_CONTAINER_REF',
- CONTAINER_ID: 'pipeline-graph-container',
+ BASE_CONTAINER_ID: 'pipeline-graph-container',
+ PIPELINE_ID: 0,
STROKE_WIDTH: 2,
errorTexts: {
[DRAW_FAILURE]: __('Could not draw the lines for job relationships'),
@@ -36,33 +36,16 @@ export default {
return {
failureType: null,
highlightedJob: null,
- links: [],
- needsObject: null,
- height: 0,
- width: 0,
+ highlightedJobs: [],
+ measurements: {
+ height: 0,
+ width: 0,
+ },
};
},
computed: {
- hideGraph() {
- // We won't even try to render the graph with these condition
- // because it would cause additional errors down the line for the user
- // which is confusing.
- return this.isPipelineDataEmpty || this.isInvalidCiConfig;
- },
- pipelineStages() {
- return this.pipelineData?.stages || [];
- },
- isPipelineDataEmpty() {
- return !this.isInvalidCiConfig && this.pipelineStages.length === 0;
- },
- isInvalidCiConfig() {
- return this.pipelineData?.status === CI_CONFIG_STATUS_INVALID;
- },
- hasError() {
- return this.failureType;
- },
- hasHighlightedJob() {
- return Boolean(this.highlightedJob);
+ containerId() {
+ return `${this.$options.BASE_CONTAINER_ID}-${this.$options.PIPELINE_ID}`;
},
failure() {
switch (this.failureType) {
@@ -92,28 +75,26 @@ export default {
};
}
},
- viewBox() {
- return [0, 0, this.width, this.height];
+ hasError() {
+ return this.failureType;
},
- highlightedJobs() {
- // If you are hovering on a job, then the jobs we want to highlight are:
- // The job you are currently hovering + all of its needs.
- return [this.highlightedJob, ...this.needsObject[this.highlightedJob]];
+ hasHighlightedJob() {
+ return Boolean(this.highlightedJob);
},
- highlightedLinks() {
- // If you are hovering on a job, then the links we want to highlight are:
- // All the links whose `source` and `target` are highlighted jobs.
- if (this.hasHighlightedJob) {
- const filteredLinks = this.links.filter((link) => {
- return (
- this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target)
- );
- });
-
- return filteredLinks.map((link) => link.ref);
- }
-
- return [];
+ hideGraph() {
+ // We won't even try to render the graph with these condition
+ // because it would cause additional errors down the line for the user
+ // which is confusing.
+ return this.isPipelineDataEmpty || this.isInvalidCiConfig;
+ },
+ isInvalidCiConfig() {
+ return this.pipelineData?.status === CI_CONFIG_STATUS_INVALID;
+ },
+ isPipelineDataEmpty() {
+ return !this.isInvalidCiConfig && this.pipelineStages.length === 0;
+ },
+ pipelineStages() {
+ return this.pipelineData?.stages || [];
},
},
watch: {
@@ -127,21 +108,17 @@ export default {
} else {
this.$nextTick(() => {
this.computeGraphDimensions();
- this.prepareLinkData();
});
}
},
},
},
methods: {
- prepareLinkData() {
- try {
- const arrayOfJobs = this.pipelineStages.flatMap(({ groups }) => groups);
- const parsedData = parseData(arrayOfJobs);
- this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID);
- } catch {
- this.reportFailure(DRAW_FAILURE);
- }
+ computeGraphDimensions() {
+ this.measurements = {
+ width: this.$refs[this.$options.CONTAINER_REF].scrollWidth,
+ height: this.$refs[this.$options.CONTAINER_REF].scrollHeight,
+ };
},
getStageBackgroundClasses(index) {
const { length } = this.pipelineStages;
@@ -161,22 +138,14 @@ export default {
return '';
},
- highlightNeeds(uniqueJobId) {
- // The first time we hover, we create the object where
- // we store all the data to properly highlight the needs.
- if (!this.needsObject) {
- const jobs = createJobsHash(this.pipelineStages);
- this.needsObject = generateJobNeedsDict(jobs) ?? {};
- }
-
- this.highlightedJob = uniqueJobId;
+ isJobHighlighted(jobName) {
+ return this.highlightedJobs.includes(jobName);
},
- removeHighlightNeeds() {
- this.highlightedJob = null;
+ onError(error) {
+ this.reportFailure(error.type);
},
- computeGraphDimensions() {
- this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}`;
- this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}`;
+ removeHoveredJob() {
+ this.highlightedJob = null;
},
reportFailure(errorType) {
this.failureType = errorType;
@@ -184,17 +153,11 @@ export default {
resetFailure() {
this.failureType = null;
},
- isJobHighlighted(jobName) {
- return this.highlightedJobs.includes(jobName);
- },
- isLinkHighlighted(linkRef) {
- return this.highlightedLinks.includes(linkRef);
+ setHoveredJob(jobName) {
+ this.highlightedJob = jobName;
},
- getLinkClasses(link) {
- return [
- this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : 'gl-stroke-gray-200',
- { 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
- ];
+ updateHighlightedJobs(jobs) {
+ this.highlightedJobs = jobs;
},
},
};
@@ -211,48 +174,47 @@ export default {
</gl-alert>
<div
v-if="!hideGraph"
- :id="$options.CONTAINER_ID"
+ :id="containerId"
:ref="$options.CONTAINER_REF"
- class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7"
data-testid="graph-container"
>
- <svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
- <path
- v-for="link in links"
- :key="link.path"
- :ref="link.ref"
- :d="link.path"
- class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="getLinkClasses(link)"
- :stroke-width="$options.STROKE_WIDTH"
- />
- </svg>
- <div
- v-for="(stage, index) in pipelineStages"
- :key="`${stage.name}-${index}`"
- class="gl-flex-direction-column"
+ <links-layer
+ :pipeline-data="pipelineStages"
+ :pipeline-id="$options.PIPELINE_ID"
+ :container-id="containerId"
+ :container-measurements="measurements"
+ :highlighted-job="highlightedJob"
+ @highlightedJobsChange="updateHighlightedJobs"
+ @error="onError"
>
<div
- class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
- :class="getStageBackgroundClasses(index)"
- data-testid="stage-background"
- >
- <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
- </div>
- <div
- class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
+ v-for="(stage, index) in pipelineStages"
+ :key="`${stage.name}-${index}`"
+ class="gl-flex-direction-column"
>
- <job-pill
- v-for="group in stage.groups"
- :key="group.name"
- :job-name="group.name"
- :is-highlighted="hasHighlightedJob && isJobHighlighted(group.name)"
- :is-faded-out="hasHighlightedJob && !isJobHighlighted(group.name)"
- @on-mouse-enter="highlightNeeds"
- @on-mouse-leave="removeHighlightNeeds"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
+ :class="getStageBackgroundClasses(index)"
+ data-testid="stage-background"
+ >
+ <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
+ </div>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
+ >
+ <job-pill
+ v-for="group in stage.groups"
+ :key="group.name"
+ :job-name="group.name"
+ :pipeline-id="$options.PIPELINE_ID"
+ :is-highlighted="hasHighlightedJob && isJobHighlighted(group.name)"
+ :is-faded-out="hasHighlightedJob && !isJobHighlighted(group.name)"
+ @on-mouse-enter="setHoveredJob"
+ @on-mouse-leave="removeHoveredJob"
+ />
+ </div>
</div>
- </div>
+ </links-layer>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue
index 3590e2c4632..ad3e6713e45 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue
@@ -30,12 +30,16 @@ export default {
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
+ v-bind="$attrs"
:width="width"
:height="$options.chartContainerHeight"
:data="chartData"
:include-legend-avg-max="false"
:option="areaChartOptions"
- />
+ >
+ <slot slot="tooltip-title" name="tooltip-title"></slot>
+ <slot slot="tooltip-content" name="tooltip-content"></slot>
+ </gl-area-chart>
</resizable-chart-container>
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
index 43b36da8b2c..f4fd57e4cdc 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
@@ -41,10 +41,14 @@ export default {
<gl-segmented-control v-model="selectedChart" :options="chartRanges" class="gl-mb-4" />
<ci-cd-analytics-area-chart
v-if="chart"
+ v-bind="$attrs"
:chart-data="chart.data"
:area-chart-options="chartOptions"
>
{{ dateRange }}
+
+ <slot slot="tooltip-title" name="tooltip-title"></slot>
+ <slot slot="tooltip-content" name="tooltip-content"></slot>
</ci-cd-analytics-area-chart>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 55e81efece2..ee90d734ecb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,6 +1,5 @@
<script>
-import { GlButton, GlModalDirective, GlSkeletonLoader, GlPopover, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import userPermissionsQuery from '../../queries/permissions.query.graphql';
@@ -13,8 +12,6 @@ export default {
GlSkeletonLoader,
StatusIcon,
GlButton,
- GlPopover,
- GlLink,
},
directives: {
GlModalDirective,
@@ -93,24 +90,12 @@ export default {
return this.mr.sourceBranchProtected;
},
- popoverTitle() {
- return s__(
- 'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.',
- );
- },
showResolveButton() {
- return this.mr.conflictResolutionPath && this.canPushToSourceBranch;
- },
- showPopover() {
- return this.showResolveButton && this.sourceBranchProtected;
+ return (
+ this.mr.conflictResolutionPath && this.canPushToSourceBranch && !this.sourceBranchProtected
+ );
},
},
- i18n: {
- title: s__(
- 'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.',
- ),
- linkText: s__('mrWidget|Learn more about resolving conflicts'),
- },
};
</script>
<template>
@@ -141,28 +126,13 @@ export default {
}}
</span>
</span>
- <span v-if="showResolveButton" ref="popover">
- <gl-button
- :href="mr.conflictResolutionPath"
- :disabled="sourceBranchProtected"
- data-testid="resolve-conflicts-button"
- >
- {{ s__('mrWidget|Resolve conflicts') }}
- </gl-button>
- <gl-popover v-if="showPopover" :target="() => $refs.popover" placement="top">
- <template #title>
- <div class="gl-font-weight-normal gl-font-base">
- {{ $options.i18n.title }}
- </div>
- </template>
-
- <div class="gl-text-center">
- <gl-link :href="mr.conflictsDocsPath" target="_blank" rel="noopener noreferrer">
- {{ $options.i18n.linkText }}
- </gl-link>
- </div>
- </gl-popover>
- </span>
+ <gl-button
+ v-if="showResolveButton"
+ :href="mr.conflictResolutionPath"
+ data-testid="resolve-conflicts-button"
+ >
+ {{ s__('mrWidget|Resolve conflicts') }}
+ </gl-button>
<gl-button
v-if="canMerge"
v-gl-modal-directive="'modal-merge-info'"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index d11a7d7873a..751f8082e1a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -54,22 +54,25 @@ export default {
},
manual: true,
result({ data }) {
+ if (Object.keys(this.state).length === 0) {
+ this.removeSourceBranch =
+ data.project.mergeRequest.shouldRemoveSourceBranch ||
+ data.project.mergeRequest.forceRemoveSourceBranch ||
+ false;
+ this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
+ this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
+ this.isSquashReadOnly = data.project.squashReadOnly;
+ this.squashCommitMessage = data.project.mergeRequest.defaultSquashCommitMessage;
+ }
+
this.state = {
...data.project.mergeRequest,
mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled,
onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds,
};
- this.removeSourceBranch =
- data.project.mergeRequest.shouldRemoveSourceBranch ||
- data.project.mergeRequest.forceRemoveSourceBranch ||
- false;
- this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
- this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
- this.isSquashReadOnly = data.project.squashReadOnly;
- this.squashCommitMessage = data.project.mergeRequest.defaultSquashCommitMessage;
this.loading = false;
- if (this.state.mergeTrainsCount !== null) {
+ if (this.state.mergeTrainsCount !== null && this.state.mergeTrainsCount !== undefined) {
this.initPolling();
}
},
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
index ff0626167a9..76f152e5453 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
@@ -1,4 +1,4 @@
-query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) {
+query getRunnerPlatforms {
runnerPlatforms {
nodes {
name
@@ -11,10 +11,4 @@ query getRunnerPlatforms($projectPath: ID!, $groupPath: ID!) {
}
}
}
- project(fullPath: $projectPath) {
- id
- }
- group(fullPath: $groupPath) {
- id
- }
}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
index 643c1991807..c0248a35e3f 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
@@ -1,15 +1,5 @@
-query runnerSetupInstructions(
- $platform: String!
- $architecture: String!
- $projectId: ID!
- $groupId: ID!
-) {
- runnerSetup(
- platform: $platform
- architecture: $architecture
- projectId: $projectId
- groupId: $groupId
- ) {
+query runnerSetupInstructions($platform: String!, $architecture: String!) {
+ runnerSetup(platform: $platform, architecture: $architecture) {
installInstructions
registerInstructions
}
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
index 662c9f595ea..d886a67fff7 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
@@ -1,156 +1,31 @@
<script>
-import {
- GlAlert,
- GlButton,
- GlModal,
- GlModalDirective,
- GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
- GlIcon,
-} from '@gitlab/ui';
-import { isEmpty } from 'lodash';
-import { __, s__ } from '~/locale';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
-import {
- PLATFORMS_WITHOUT_ARCHITECTURES,
- INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
-} from './constants';
-import getRunnerPlatforms from './graphql/queries/get_runner_platforms.query.graphql';
-import getRunnerSetupInstructions from './graphql/queries/get_runner_setup.query.graphql';
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import RunnerInstructionsModal from './runner_instructions_modal.vue';
export default {
components: {
- GlAlert,
GlButton,
- GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- GlIcon,
- ModalCopyButton,
+ RunnerInstructionsModal,
},
directives: {
GlModalDirective,
},
- inject: {
- projectPath: {
- default: '',
- },
- groupPath: {
- default: '',
- },
- },
- apollo: {
- runnerPlatforms: {
- query: getRunnerPlatforms,
- variables() {
- return {
- projectPath: this.projectPath,
- groupPath: this.groupPath,
- };
- },
- error() {
- this.showAlert = true;
- },
- result({ data }) {
- this.project = data?.project;
- this.group = data?.group;
-
- this.selectPlatform(this.platforms[0].name);
- },
- },
+ modalId: 'runner-instructions-modal',
+ i18n: {
+ buttonText: s__('Runners|Show Runner installation instructions'),
},
data() {
return {
- showAlert: false,
- selectedPlatformArchitectures: [],
- selectedPlatform: {
- name: '',
- },
- selectedArchitecture: {},
- runnerPlatforms: {},
- instructions: {},
- project: {},
- group: {},
+ opened: false,
};
},
- computed: {
- isPlatformSelected() {
- return Object.keys(this.selectedPlatform).length > 0;
- },
- instructionsEmpty() {
- return isEmpty(this.instructions);
- },
- groupId() {
- return this.group?.id ?? '';
- },
- projectId() {
- return this.project?.id ?? '';
- },
- platforms() {
- return this.runnerPlatforms?.nodes;
- },
- hasArchitecureList() {
- return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatform?.name);
- },
- instructionsWithoutArchitecture() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.instructions;
- },
- runnerInstallationLink() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform.name]?.link;
- },
- },
methods: {
- selectPlatform(name) {
- this.selectedPlatform = this.platforms.find((platform) => platform.name === name);
- if (this.hasArchitecureList) {
- this.selectedPlatformArchitectures = this.selectedPlatform?.architectures?.nodes;
- [this.selectedArchitecture] = this.selectedPlatformArchitectures;
- this.selectArchitecture(this.selectedArchitecture);
- }
- },
- selectArchitecture(architecture) {
- this.selectedArchitecture = architecture;
-
- this.$apollo.addSmartQuery('instructions', {
- variables() {
- return {
- platform: this.selectedPlatform.name,
- architecture: this.selectedArchitecture.name,
- projectId: this.projectId,
- groupId: this.groupId,
- };
- },
- query: getRunnerSetupInstructions,
- update(data) {
- return data?.runnerSetup;
- },
- error() {
- this.showAlert = true;
- },
- });
- },
- toggleAlert(state) {
- this.showAlert = state;
+ onClick() {
+ // lazily mount modal to prevent premature instructions requests
+ this.opened = true;
},
},
- modalId: 'installation-instructions-modal',
- i18n: {
- installARunner: s__('Runners|Install a Runner'),
- architecture: s__('Runners|Architecture'),
- downloadInstallBinary: s__('Runners|Download and Install Binary'),
- downloadLatestBinary: s__('Runners|Download Latest Binary'),
- registerRunner: s__('Runners|Register Runner'),
- method: __('Method'),
- fetchError: s__('Runners|An error has occurred fetching instructions'),
- instructions: s__('Runners|Show Runner installation instructions'),
- copyInstructions: s__('Runners|Copy instructions'),
- },
- closeButton: {
- text: __('Close'),
- attributes: [{ variant: 'default' }],
- },
};
</script>
<template>
@@ -159,101 +34,10 @@ export default {
v-gl-modal-directive="$options.modalId"
class="gl-mt-4"
data-testid="show-modal-button"
+ @click="onClick"
>
- {{ $options.i18n.instructions }}
+ {{ $options.i18n.buttonText }}
</gl-button>
- <gl-modal
- :modal-id="$options.modalId"
- :title="$options.i18n.installARunner"
- :action-secondary="$options.closeButton"
- >
- <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
- {{ $options.i18n.fetchError }}
- </gl-alert>
- <h5>{{ __('Environment') }}</h5>
- <gl-button-group class="gl-mb-5">
- <gl-button
- v-for="platform in platforms"
- :key="platform.name"
- data-testid="platform-button"
- @click="selectPlatform(platform.name)"
- >
- {{ platform.humanReadableName }}
- </gl-button>
- </gl-button-group>
- <template v-if="hasArchitecureList">
- <template v-if="isPlatformSelected">
- <h5>
- {{ $options.i18n.architecture }}
- </h5>
- <gl-dropdown class="gl-mb-5" :text="selectedArchitecture.name">
- <gl-dropdown-item
- v-for="architecture in selectedPlatformArchitectures"
- :key="architecture.name"
- data-testid="architecture-dropdown-item"
- @click="selectArchitecture(architecture)"
- >
- {{ architecture.name }}
- </gl-dropdown-item>
- </gl-dropdown>
- <div class="gl-display-flex gl-align-items-center gl-mb-5">
- <h5>{{ $options.i18n.downloadInstallBinary }}</h5>
- <gl-button
- class="gl-ml-auto"
- :href="selectedArchitecture.downloadLocation"
- download
- data-testid="binary-download-button"
- >
- {{ $options.i18n.downloadLatestBinary }}
- </gl-button>
- </div>
- </template>
- <template v-if="!instructionsEmpty">
- <div class="gl-display-flex">
- <pre
- class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
- data-testid="binary-instructions"
- >{{ instructions.installInstructions }}</pre
- >
- <modal-copy-button
- :title="$options.i18n.copyInstructions"
- :text="instructions.installInstructions"
- :modal-id="$options.modalId"
- css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
- category="tertiary"
- />
- </div>
-
- <hr />
- <h5 class="gl-mb-5">{{ $options.i18n.registerRunner }}</h5>
- <h5 class="gl-mb-5">{{ $options.i18n.method }}</h5>
- <div class="gl-display-flex">
- <pre
- class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
- data-testid="runner-instructions"
- >
- {{ instructions.registerInstructions }}
- </pre
- >
- <modal-copy-button
- :title="$options.i18n.copyInstructions"
- :text="instructions.registerInstructions"
- :modal-id="$options.modalId"
- css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
- category="tertiary"
- />
- </div>
- </template>
- </template>
- <template v-else>
- <div>
- <p>{{ instructionsWithoutArchitecture }}</p>
- <gl-button :href="runnerInstallationLink">
- <gl-icon name="external-link" />
- {{ s__('Runners|View installation instructions') }}
- </gl-button>
- </div>
- </template>
- </gl-modal>
+ <runner-instructions-modal v-if="opened" :modal-id="$options.modalId" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
new file mode 100644
index 00000000000..795b4f58ac5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -0,0 +1,249 @@
+<script>
+import {
+ GlAlert,
+ GlButton,
+ GlModal,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlLoadingIcon,
+ GlSkeletonLoader,
+} from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+import { __, s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import {
+ PLATFORMS_WITHOUT_ARCHITECTURES,
+ INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
+} from './constants';
+import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructionsQuery from './graphql/queries/get_runner_setup.query.graphql';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlIcon,
+ GlLoadingIcon,
+ GlSkeletonLoader,
+ ModalCopyButton,
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ platforms: {
+ query: getRunnerPlatformsQuery,
+ update(data) {
+ return data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
+ return {
+ name,
+ humanReadableName,
+ architectures: architectures?.nodes || [],
+ };
+ });
+ },
+ result() {
+ // Select first platform by default
+ if (this.platforms?.[0]) {
+ this.selectPlatform(this.platforms[0]);
+ }
+ },
+ error() {
+ this.toggleAlert(true);
+ },
+ },
+ instructions: {
+ query: getRunnerSetupInstructionsQuery,
+ skip() {
+ return !this.selectedPlatform;
+ },
+ variables() {
+ return {
+ platform: this.selectedPlatformName,
+ architecture: this.selectedArchitectureName || '',
+ };
+ },
+ update(data) {
+ return data?.runnerSetup;
+ },
+ error() {
+ this.toggleAlert(true);
+ },
+ },
+ },
+ data() {
+ return {
+ platforms: [],
+ selectedPlatform: null,
+ selectedArchitecture: null,
+ showAlert: false,
+ instructions: {},
+ };
+ },
+ computed: {
+ platformsEmpty() {
+ return isEmpty(this.platforms);
+ },
+ instructionsEmpty() {
+ return isEmpty(this.instructions);
+ },
+ selectedPlatformName() {
+ return this.selectedPlatform?.name;
+ },
+ selectedArchitectureName() {
+ return this.selectedArchitecture?.name;
+ },
+ hasArchitecureList() {
+ return !PLATFORMS_WITHOUT_ARCHITECTURES.includes(this.selectedPlatformName);
+ },
+ instructionsWithoutArchitecture() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.instructions;
+ },
+ runnerInstallationLink() {
+ return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatformName]?.link;
+ },
+ },
+ methods: {
+ selectPlatform(platform) {
+ this.selectedPlatform = platform;
+
+ if (!platform.architectures?.some(({ name }) => name === this.selectedArchitectureName)) {
+ // Select first architecture when current value is not available
+ this.selectArchitecture(platform.architectures[0]);
+ }
+ },
+ selectArchitecture(architecture) {
+ this.selectedArchitecture = architecture;
+ },
+ toggleAlert(state) {
+ this.showAlert = state;
+ },
+ },
+ i18n: {
+ installARunner: s__('Runners|Install a runner'),
+ architecture: s__('Runners|Architecture'),
+ downloadInstallBinary: s__('Runners|Download and install binary'),
+ downloadLatestBinary: s__('Runners|Download latest binary'),
+ registerRunnerCommand: s__('Runners|Command to register runner'),
+ fetchError: s__('Runners|An error has occurred fetching instructions'),
+ copyInstructions: s__('Runners|Copy instructions'),
+ },
+ closeButton: {
+ text: __('Close'),
+ attributes: [{ variant: 'default' }],
+ },
+};
+</script>
+<template>
+ <gl-modal
+ :modal-id="modalId"
+ :title="$options.i18n.installARunner"
+ :action-secondary="$options.closeButton"
+ >
+ <gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
+ {{ $options.i18n.fetchError }}
+ </gl-alert>
+
+ <gl-skeleton-loader v-if="platformsEmpty && $apollo.loading" />
+
+ <template v-if="!platformsEmpty">
+ <h5>
+ {{ __('Environment') }}
+ </h5>
+ <gl-button-group class="gl-mb-3">
+ <gl-button
+ v-for="platform in platforms"
+ :key="platform.name"
+ :selected="selectedPlatform && selectedPlatform.name === platform.name"
+ data-testid="platform-button"
+ @click="selectPlatform(platform)"
+ >
+ {{ platform.humanReadableName }}
+ </gl-button>
+ </gl-button-group>
+ </template>
+ <template v-if="hasArchitecureList">
+ <template v-if="selectedPlatform">
+ <h5>
+ {{ $options.i18n.architecture }}
+ <gl-loading-icon v-if="$apollo.loading" inline />
+ </h5>
+
+ <gl-dropdown class="gl-mb-3" :text="selectedArchitectureName">
+ <gl-dropdown-item
+ v-for="architecture in selectedPlatform.architectures"
+ :key="architecture.name"
+ :is-check-item="true"
+ :is-checked="selectedArchitectureName === architecture.name"
+ data-testid="architecture-dropdown-item"
+ @click="selectArchitecture(architecture)"
+ >
+ {{ architecture.name }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <div class="gl-display-flex gl-align-items-center gl-mb-3">
+ <h5>{{ $options.i18n.downloadInstallBinary }}</h5>
+ <gl-button
+ class="gl-ml-auto"
+ :href="selectedArchitecture.downloadLocation"
+ download
+ icon="download"
+ data-testid="binary-download-button"
+ >
+ {{ $options.i18n.downloadLatestBinary }}
+ </gl-button>
+ </div>
+ </template>
+ <template v-if="!instructionsEmpty">
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="binary-instructions"
+ >{{ instructions.installInstructions }}</pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.installInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+
+ <h5 class="gl-mb-3">{{ $options.i18n.registerRunnerCommand }}</h5>
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-fill-1 gl-white-space-pre-line"
+ data-testid="register-command"
+ >{{ instructions.registerInstructions }}</pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.registerInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+ </template>
+ </template>
+ <template v-else>
+ <div>
+ <p>{{ instructionsWithoutArchitecture }}</p>
+ <gl-button :href="runnerInstallationLink">
+ <gl-icon name="external-link" />
+ {{ s__('Runners|View installation instructions') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index a1a6a057171..62ec03206c4 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -35,7 +35,7 @@ module Registrations
end
def update_params
- params.require(:user).permit(:role, :other_role, :setup_for_company)
+ params.require(:user).permit(:role, :other_role, :setup_for_company, :email_opted_in)
end
def requires_confirmation?(user)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 8642ea11a57..9e112f4e5f0 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -244,7 +244,7 @@ class IssuableFinder
# These are "helper" params that modify the results, like :in and :search. They usually come in at the top-level
# params, but if they do come in inside the `:not` params, the inner ones should take precedence.
- not_helpers = params.slice(*NEGATABLE_PARAMS_HELPER_KEYS).merge(params[:not].slice(*NEGATABLE_PARAMS_HELPER_KEYS))
+ not_helpers = params.slice(*NEGATABLE_PARAMS_HELPER_KEYS).merge(params[:not].to_h.slice(*NEGATABLE_PARAMS_HELPER_KEYS))
not_helpers.each do |key, value|
not_params[key] = value unless not_params[key].present?
end
diff --git a/app/finders/repositories/branch_names_finder.rb b/app/finders/repositories/branch_names_finder.rb
new file mode 100644
index 00000000000..5bb67425aa5
--- /dev/null
+++ b/app/finders/repositories/branch_names_finder.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Repositories
+ class BranchNamesFinder
+ attr_reader :repository, :params
+
+ def initialize(repository, params = {})
+ @repository = repository
+ @params = params
+ end
+
+ def execute
+ return unless search
+
+ repository.search_branch_names(search)
+ end
+
+ private
+
+ def search
+ @params[:search].presence
+ end
+ end
+end
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index 52f7fa64864..a9eea4ae4b8 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -3,6 +3,7 @@
module Resolvers
class MergeRequestsResolver < BaseResolver
include ResolvesMergeRequests
+ extend ::Gitlab::Graphql::NegatableArguments
type ::Types::MergeRequestType.connection_type, null: true
@@ -68,6 +69,16 @@ module Resolvers
required: false,
default_value: :created_desc
+ negated do
+ argument :labels, [GraphQL::STRING_TYPE],
+ required: false,
+ as: :label_name,
+ description: 'Array of label names. All resolved merge requests will not have these labels.'
+ argument :milestone_title, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Title of the milestone.'
+ end
+
def self.single
::Resolvers::MergeRequestResolver
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 8d22bda279f..09f91f350bd 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -24,11 +24,7 @@ module AvatarsHelper
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
return gravatar_icon(email, size, scale) if email.nil?
- if Feature.enabled?(:avatar_cache_for_email, @current_user, type: :development)
- Gitlab::AvatarCache.by_email(email, size, scale, only_path) do
- avatar_icon_by_user_email_or_gravatar(email, size, scale, only_path: only_path)
- end
- else
+ Gitlab::AvatarCache.by_email(email, size, scale, only_path) do
avatar_icon_by_user_email_or_gravatar(email, size, scale, only_path: only_path)
end
end
diff --git a/app/helpers/in_product_marketing_helper.rb b/app/helpers/in_product_marketing_helper.rb
index 061404e989d..497be7e7c07 100644
--- a/app/helpers/in_product_marketing_helper.rb
+++ b/app/helpers/in_product_marketing_helper.rb
@@ -313,7 +313,8 @@ module InProductMarketingHelper
end
def unsubscribe_link(format)
- link(s_('InProductMarketing|unsubscribe'), '%tag_unsubscribe_url%', format)
+ unsubscribe_url = Gitlab.com? ? '%tag_unsubscribe_url%' : profile_notifications_url
+ link(s_('InProductMarketing|unsubscribe'), unsubscribe_url, format)
end
def link(text, link, format)
diff --git a/app/mailers/emails/in_product_marketing.rb b/app/mailers/emails/in_product_marketing.rb
index c55e178b70d..d21c3d13b10 100644
--- a/app/mailers/emails/in_product_marketing.rb
+++ b/app/mailers/emails/in_product_marketing.rb
@@ -6,6 +6,8 @@ module Emails
FROM_ADDRESS = 'GitLab <team@gitlab.com>'
CUSTOM_HEADERS = {
+ from: FROM_ADDRESS,
+ reply_to: FROM_ADDRESS,
'X-Mailgun-Track' => 'yes',
'X-Mailgun-Track-Clicks' => 'yes',
'X-Mailgun-Track-Opens' => 'yes',
@@ -25,7 +27,8 @@ module Emails
private
def mail_to(to:, subject:)
- mail(to: to, subject: subject, from: FROM_ADDRESS, reply_to: FROM_ADDRESS, **CUSTOM_HEADERS) do |format|
+ custom_headers = Gitlab.com? ? CUSTOM_HEADERS : {}
+ mail(to: to, subject: subject, **custom_headers) do |format|
format.html { render layout: nil }
format.text { render layout: nil }
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index c106c08c04a..fdc418029be 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -131,7 +131,6 @@ module Avatarable
def clear_avatar_caches
return unless respond_to?(:verified_emails) && verified_emails.any? && avatar_changed?
- return unless Feature.enabled?(:avatar_cache_for_email, self, type: :development)
Gitlab::AvatarCache.delete_by_email(*verified_emails)
end
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
new file mode 100644
index 00000000000..cd489e3a7f3
--- /dev/null
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+#
+# Cascading attributes enables managing settings in a flexible way.
+#
+# - Instance administrator can define an instance-wide default setting, or
+# lock the setting to prevent change by group owners.
+# - Group maintainers/owners can define a default setting for their group, or
+# lock the setting to prevent change by sub-group maintainers/owners.
+#
+# Behavior:
+#
+# - When a group does not have a value (value is `nil`), cascade up the
+# hierarchy to find the first non-nil value.
+# - Settings can be locked at any level to prevent groups/sub-groups from
+# overriding.
+# - If the setting isn't locked, the default can be overridden.
+# - An instance administrator or group maintainer/owner can push settings values
+# to groups/sub-groups to override existing values, even when the setting
+# is not otherwise locked.
+#
+module CascadingNamespaceSettingAttribute
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ class_methods do
+ def cascading_settings_feature_enabled?
+ ::Feature.enabled?(:cascading_namespace_settings, default_enabled: false)
+ end
+
+ private
+
+ # Facilitates the cascading lookup of values and,
+ # similar to Rails' `attr_accessor`, defines convenience methods such as
+ # a reader, writer, and validators.
+ #
+ # Example: `cascading_attr :delayed_project_removal`
+ #
+ # Public methods defined:
+ # - `delayed_project_removal`
+ # - `delayed_project_removal=`
+ # - `delayed_project_removal_locked?`
+ # - `delayed_project_removal_locked_by_ancestor?`
+ # - `delayed_project_removal_locked_by_application_setting?`
+ # - `delayed_project_removal?` (only defined for boolean attributes)
+ # - `delayed_project_removal_locked_ancestor` - Returns locked namespace settings object (only namespace_id)
+ #
+ # Defined validators ensure attribute value cannot be updated if locked by
+ # an ancestor or application settings.
+ #
+ # Requires database columns be present in both `namespace_settings` and
+ # `application_settings`.
+ def cascading_attr(*attributes)
+ attributes.map(&:to_sym).each do |attribute|
+ # public methods
+ define_attr_reader(attribute)
+ define_attr_writer(attribute)
+ define_lock_methods(attribute)
+ alias_boolean(attribute)
+
+ # private methods
+ define_validator_methods(attribute)
+ define_after_update(attribute)
+
+ validate :"#{attribute}_changeable?"
+ validate :"lock_#{attribute}_changeable?"
+
+ after_update :"clear_descendant_#{attribute}_locks", if: -> { saved_change_to_attribute?("lock_#{attribute}", to: true) }
+ end
+ end
+
+ # The cascading attribute reader method handles lookups
+ # with the following criteria:
+ #
+ # 1. Returns the dirty value, if the attribute has changed.
+ # 2. Return locked ancestor value.
+ # 3. Return locked instance-level application settings value.
+ # 4. Return this namespace's attribute, if not nil.
+ # 5. Return value from nearest ancestor where value is not nil.
+ # 6. Return instance-level application setting.
+ def define_attr_reader(attribute)
+ define_method(attribute) do
+ strong_memoize(attribute) do
+ next self[attribute] unless self.class.cascading_settings_feature_enabled?
+
+ next self[attribute] if will_save_change_to_attribute?(attribute)
+ next locked_value(attribute) if cascading_attribute_locked?(attribute)
+ next self[attribute] unless self[attribute].nil?
+
+ cascaded_value = cascaded_ancestor_value(attribute)
+ next cascaded_value unless cascaded_value.nil?
+
+ application_setting_value(attribute)
+ end
+ end
+ end
+
+ def define_attr_writer(attribute)
+ define_method("#{attribute}=") do |value|
+ clear_memoization(attribute)
+
+ super(value)
+ end
+ end
+
+ def define_lock_methods(attribute)
+ define_method("#{attribute}_locked?") do
+ cascading_attribute_locked?(attribute)
+ end
+
+ define_method("#{attribute}_locked_by_ancestor?") do
+ locked_by_ancestor?(attribute)
+ end
+
+ define_method("#{attribute}_locked_by_application_setting?") do
+ locked_by_application_setting?(attribute)
+ end
+
+ define_method("#{attribute}_locked_ancestor") do
+ locked_ancestor(attribute)
+ end
+ end
+
+ def alias_boolean(attribute)
+ return unless Gitlab::Database.exists? && type_for_attribute(attribute).type == :boolean
+
+ alias_method :"#{attribute}?", attribute
+ end
+
+ # Defines two validations - one for the cascadable attribute itself and one
+ # for the lock attribute. Only allows the respective value to change if
+ # an ancestor has not already locked the value.
+ def define_validator_methods(attribute)
+ define_method("#{attribute}_changeable?") do
+ return unless cascading_attribute_changed?(attribute)
+ return unless cascading_attribute_locked?(attribute)
+
+ errors.add(attribute, s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
+ end
+
+ define_method("lock_#{attribute}_changeable?") do
+ return unless cascading_attribute_changed?("lock_#{attribute}")
+
+ if cascading_attribute_locked?(attribute)
+ return errors.add(:"lock_#{attribute}", s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
+ end
+
+ # Don't allow locking a `nil` attribute.
+ # Even if the value being locked is currently cascaded from an ancestor,
+ # it should be copied to this record to avoid the ancestor changing the
+ # value unexpectedly later.
+ return unless self[attribute].nil? && public_send("lock_#{attribute}?") # rubocop:disable GitlabSecurity/PublicSend
+
+ errors.add(attribute, s_('CascadingSettings|cannot be nil when locking the attribute'))
+ end
+
+ private :"#{attribute}_changeable?", :"lock_#{attribute}_changeable?"
+ end
+
+ # When a particular group locks the attribute, clear all sub-group locks
+ # since the higher lock takes priority.
+ def define_after_update(attribute)
+ define_method("clear_descendant_#{attribute}_locks") do
+ self.class.where(namespace_id: descendants).update_all("lock_#{attribute}" => false)
+ end
+
+ private :"clear_descendant_#{attribute}_locks"
+ end
+ end
+
+ private
+
+ def locked_value(attribute)
+ ancestor = locked_ancestor(attribute)
+ return ancestor.read_attribute(attribute) if ancestor
+
+ Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def locked_ancestor(attribute)
+ return unless self.class.cascading_settings_feature_enabled?
+ return unless namespace.has_parent?
+
+ strong_memoize(:"#{attribute}_locked_ancestor") do
+ self.class
+ .select(:namespace_id, "lock_#{attribute}", attribute)
+ .where(namespace_id: namespace_ancestor_ids)
+ .where(self.class.arel_table["lock_#{attribute}"].eq(true))
+ .limit(1).load.first
+ end
+ end
+
+ def locked_by_ancestor?(attribute)
+ return false unless self.class.cascading_settings_feature_enabled?
+
+ locked_ancestor(attribute).present?
+ end
+
+ def locked_by_application_setting?(attribute)
+ return false unless self.class.cascading_settings_feature_enabled?
+
+ Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def cascading_attribute_locked?(attribute)
+ locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
+ end
+
+ def cascading_attribute_changed?(attribute)
+ public_send("#{attribute}_changed?") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def cascaded_ancestor_value(attribute)
+ return unless namespace.has_parent?
+
+ # rubocop:disable GitlabSecurity/SqlInjection
+ self.class
+ .select(attribute)
+ .joins("join unnest(ARRAY[#{namespace_ancestor_ids.join(',')}]) with ordinality t(namespace_id, ord) USING (namespace_id)")
+ .where("#{attribute} IS NOT NULL")
+ .order('t.ord')
+ .limit(1).first&.read_attribute(attribute)
+ # rubocop:enable GitlabSecurity/SqlInjection
+ end
+
+ def application_setting_value(attribute)
+ Gitlab::CurrentSettings.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def namespace_ancestor_ids
+ strong_memoize(:namespace_ancestor_ids) do
+ namespace.self_and_ancestors(hierarchy_order: :asc).pluck(:id).reject { |id| id == namespace_id }
+ end
+ end
+
+ def descendants
+ strong_memoize(:descendants) do
+ namespace.descendants.pluck(:id)
+ end
+ end
+end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index aec701a4b86..0412f7a072b 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -43,14 +43,6 @@ module Ci
def completed_statuses
COMPLETED_STATUSES.map(&:to_sym)
end
-
- def blocked_statuses
- BLOCKED_STATUS.map(&:to_sym)
- end
-
- def completed_and_blocked_statuses
- completed_statuses + blocked_statuses
- end
end
included do
diff --git a/app/models/concerns/deprecated_assignee.rb b/app/models/concerns/deprecated_assignee.rb
index 7f12ce39c96..3f557ee9b48 100644
--- a/app/models/concerns/deprecated_assignee.rb
+++ b/app/models/concerns/deprecated_assignee.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# This module handles backward compatibility for import/export of Merge Requests after
+# This module handles backward compatibility for import/export of merge requests after
# multiple assignees feature was introduced. Also, it handles the scenarios where
# the #26496 background migration hasn't finished yet.
# Ideally, most of this code should be removed at #59457.
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 573f6a48cc0..d21f9632e18 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class NamespaceSetting < ApplicationRecord
+ include CascadingNamespaceSettingAttribute
+
+ cascading_attr :delayed_project_removal
+
belongs_to :namespace, inverse_of: :namespace_settings
validate :default_branch_name_content
@@ -9,7 +13,8 @@ class NamespaceSetting < ApplicationRecord
before_validation :normalize_default_branch_name
- NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal, :resource_access_token_creation_allowed].freeze
+ NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
+ :lock_delayed_project_removal, :resource_access_token_creation_allowed].freeze
self.primary_key = :namespace_id
diff --git a/app/models/project.rb b/app/models/project.rb
index 3a136e5fe1f..87b1d057e5e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -221,7 +221,7 @@ class Project < ApplicationRecord
has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
has_one :service_desk_setting, class_name: 'ServiceDeskSetting'
- # Merge Requests for target project should be removed with it
+ # Merge requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index b9916a54d75..e45bb9b8ce1 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -28,7 +28,7 @@ module ChatMessage
def activity
{
- title: "Merge Request #{state_or_action_text} by #{user_combined_name}",
+ title: "Merge request #{state_or_action_text} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 47106d7bdbb..29edb9ec16f 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -2,7 +2,7 @@
# Base class for CI services
# List methods you need to implement to get your CI service
-# working with GitLab Merge Requests
+# working with GitLab merge requests
class CiService < Service
default_value_for :category, 'ci'
diff --git a/app/models/repository.rb b/app/models/repository.rb
index b23b9486cfe..b2efc9b480b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -288,6 +288,10 @@ class Repository
false
end
+ def search_branch_names(pattern)
+ redis_set_cache.search('branch_names', pattern) { branch_names }
+ end
+
def languages
return [] if empty?
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 3445e7180e6..47fe40b0e57 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -160,16 +160,12 @@ class Wiki
end
def find_file(name, version = 'HEAD', load_content: true)
- if Feature.enabled?(:gitaly_find_file, user, default_enabled: :yaml)
- data_limit = load_content ? -1 : 0
- blobs = repository.blobs_at([[version, name]], blob_size_limit: data_limit)
+ data_limit = load_content ? -1 : 0
+ blobs = repository.blobs_at([[version, name]], blob_size_limit: data_limit)
- return if blobs.empty?
+ return if blobs.empty?
- Gitlab::Git::WikiFile.from_blob(blobs.first)
- else
- wiki.file(name, version)
- end
+ Gitlab::Git::WikiFile.new(blobs.first)
end
def create_page(title, content, format = :markdown, message = nil)
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index 6c2e4c69d83..03a3c9b0de8 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -22,4 +22,4 @@
method: :put, class: 'gl-button btn btn-default',
data: { confirm: _("Are you sure you want to reset the registration token?") }
-#js-install-runner{ data: { project_path: project_path, group_path: group_path } }
+#js-install-runner
diff --git a/app/views/devise/shared/_email_opted_in.html.haml b/app/views/devise/shared/_email_opted_in.html.haml
new file mode 100644
index 00000000000..6896ef21536
--- /dev/null
+++ b/app/views/devise/shared/_email_opted_in.html.haml
@@ -0,0 +1,7 @@
+- is_hidden = local_assigns.fetch(:hidden, Gitlab.dev_env_or_com?)
+
+.gl-mb-3.js-email-opt-in{ class: is_hidden ? 'hidden' : '' }
+ .gl-font-weight-bold.gl-mb-3
+ = _('Email updates (optional)')
+ = f.check_box :email_opted_in
+ = f.label :email_opted_in, _("I'd like to receive updates about GitLab via email"), class: 'gl-font-weight-normal'
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index f432915e870..390a070de02 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -20,6 +20,7 @@
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
= f.text_field :other_role, class: 'form-control'
= render_if_exists "registrations/welcome/setup_for_company", f: f
+ = render 'devise/shared/email_opted_in', f: f
.row
.form-group.col-sm-12.gl-mb-0
- if partial_exists? "registrations/welcome/button"
diff --git a/app/workers/namespaces/in_product_marketing_emails_worker.rb b/app/workers/namespaces/in_product_marketing_emails_worker.rb
index f8fa393264a..817925987f2 100644
--- a/app/workers/namespaces/in_product_marketing_emails_worker.rb
+++ b/app/workers/namespaces/in_product_marketing_emails_worker.rb
@@ -10,7 +10,7 @@ module Namespaces
def perform
return unless Gitlab::CurrentSettings.in_product_marketing_emails_enabled
- return unless Gitlab::Experimentation.active?(:in_product_marketing_emails)
+ return if Gitlab.com? && !Gitlab::Experimentation.active?(:in_product_marketing_emails)
Namespaces::InProductMarketingEmailsService.send_for_all_tracks_and_intervals
end
diff --git a/changelogs/unreleased/220647-add-prefix-to-CSS-class-of-syntax-highlighting-blocks.yml b/changelogs/unreleased/220647-add-prefix-to-CSS-class-of-syntax-highlighting-blocks.yml
new file mode 100644
index 00000000000..206af99e369
--- /dev/null
+++ b/changelogs/unreleased/220647-add-prefix-to-CSS-class-of-syntax-highlighting-blocks.yml
@@ -0,0 +1,5 @@
+---
+title: Add language- prefix to CSS class of markdown code blocks
+merge_request: 55076
+author: Camil Staps
+type: fixed
diff --git a/changelogs/unreleased/296888-resolve-conflicts-popover-does-not-show.yml b/changelogs/unreleased/296888-resolve-conflicts-popover-does-not-show.yml
new file mode 100644
index 00000000000..4ee615c3a42
--- /dev/null
+++ b/changelogs/unreleased/296888-resolve-conflicts-popover-does-not-show.yml
@@ -0,0 +1,5 @@
+---
+title: Hide "Resolve conflicts" button when source branch is protected.
+merge_request: 51121
+author: Marcin Majkowski @marcinmajkowski
+type: added
diff --git a/changelogs/unreleased/324105-add-commit-email-to-users-api.yml b/changelogs/unreleased/324105-add-commit-email-to-users-api.yml
new file mode 100644
index 00000000000..2e51863b060
--- /dev/null
+++ b/changelogs/unreleased/324105-add-commit-email-to-users-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for commit_email to Users API
+merge_request: 56272
+author:
+type: changed
diff --git a/changelogs/unreleased/324306-fj-enable-gitaly-find-file-feature-flag.yml b/changelogs/unreleased/324306-fj-enable-gitaly-find-file-feature-flag.yml
new file mode 100644
index 00000000000..ae845bcfeba
--- /dev/null
+++ b/changelogs/unreleased/324306-fj-enable-gitaly-find-file-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Enable new RPC call to retrieve wiki files
+merge_request: 56491
+author:
+type: changed
diff --git a/changelogs/unreleased/326102-fe-cleanup-runner-instructions-project-group-parameters.yml b/changelogs/unreleased/326102-fe-cleanup-runner-instructions-project-group-parameters.yml
new file mode 100644
index 00000000000..fde80656425
--- /dev/null
+++ b/changelogs/unreleased/326102-fe-cleanup-runner-instructions-project-group-parameters.yml
@@ -0,0 +1,6 @@
+---
+title: 'Improve UI of Runner Installation instructions: add a loading indicator, use
+ checkmark on selected options, reduce height of modal'
+merge_request: 58055
+author:
+type: changed
diff --git a/changelogs/unreleased/92508-enable-not-filters-for-mr-labels-graphql.yml b/changelogs/unreleased/92508-enable-not-filters-for-mr-labels-graphql.yml
new file mode 100644
index 00000000000..fdd67f370c4
--- /dev/null
+++ b/changelogs/unreleased/92508-enable-not-filters-for-mr-labels-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Add negative filters for merge requests API
+merge_request: 58021
+author:
+type: added
diff --git a/changelogs/unreleased/dblessing_cascading_settings_final.yml b/changelogs/unreleased/dblessing_cascading_settings_final.yml
new file mode 100644
index 00000000000..49f5cd5c9fc
--- /dev/null
+++ b/changelogs/unreleased/dblessing_cascading_settings_final.yml
@@ -0,0 +1,5 @@
+---
+title: Cascade delayed project removal setting lookup to parent namespace
+merge_request: 55678
+author:
+type: added
diff --git a/changelogs/unreleased/in-product-email-campaigns-self-managed.yml b/changelogs/unreleased/in-product-email-campaigns-self-managed.yml
new file mode 100644
index 00000000000..22c7332e507
--- /dev/null
+++ b/changelogs/unreleased/in-product-email-campaigns-self-managed.yml
@@ -0,0 +1,5 @@
+---
+title: Send in-product marketing emails to guide users setting up their groups
+merge_request: 53715
+author:
+type: added
diff --git a/changelogs/unreleased/mc-backstage-use-redis-in-branch-finder.yml b/changelogs/unreleased/mc-backstage-use-redis-in-branch-finder.yml
new file mode 100644
index 00000000000..93f44b0f720
--- /dev/null
+++ b/changelogs/unreleased/mc-backstage-use-redis-in-branch-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Create finder for searching branch names via redis.
+merge_request: 58439
+author:
+type: performance
diff --git a/changelogs/unreleased/rails-save-bang-initializers.yml b/changelogs/unreleased/rails-save-bang-initializers.yml
new file mode 100644
index 00000000000..d3a249b5a85
--- /dev/null
+++ b/changelogs/unreleased/rails-save-bang-initializers.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Rails/SaveBang rubocop offenses in spec/initializers
+merge_request: 58049
+author: Abdul Wadood @abdulwd
+type: fixed
diff --git a/changelogs/unreleased/remove-avatar-cache-ff.yml b/changelogs/unreleased/remove-avatar-cache-ff.yml
new file mode 100644
index 00000000000..40f2d618ab4
--- /dev/null
+++ b/changelogs/unreleased/remove-avatar-cache-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Enable cached avatar lookups by email
+merge_request: 58659
+author:
+type: performance
diff --git a/changelogs/unreleased/sy-on-call-usage-ping.yml b/changelogs/unreleased/sy-on-call-usage-ping.yml
new file mode 100644
index 00000000000..fe3dd8521d7
--- /dev/null
+++ b/changelogs/unreleased/sy-on-call-usage-ping.yml
@@ -0,0 +1,5 @@
+---
+title: Add count of unique users to receive on-call notification to usage ping
+merge_request: 58606
+author:
+type: changed
diff --git a/config/feature_flags/development/cascading_namespace_settings.yml b/config/feature_flags/development/cascading_namespace_settings.yml
new file mode 100644
index 00000000000..ca4ad32d7c3
--- /dev/null
+++ b/config/feature_flags/development/cascading_namespace_settings.yml
@@ -0,0 +1,8 @@
+---
+name: cascading_namespace_settings
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55678
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321724
+milestone: '13.11'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/gitaly_find_file.yml b/config/feature_flags/development/gitaly_find_file.yml
deleted file mode 100644
index 8d0bc0c5b53..00000000000
--- a/config/feature_flags/development/gitaly_find_file.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: gitaly_find_file
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56321
-rollout_issue_url:
-milestone: '13.10'
-type: development
-group: group::editor
-default_enabled: false
diff --git a/config/feature_flags/development/avatar_cache_for_email.yml b/config/feature_flags/development/project_sidebar_refactor.yml
index d0285b5bb0f..88cca9d8d13 100644
--- a/config/feature_flags/development/avatar_cache_for_email.yml
+++ b/config/feature_flags/development/project_sidebar_refactor.yml
@@ -1,8 +1,8 @@
---
-name: avatar_cache_for_email
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55184
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323185
-milestone: '13.10'
+name: project_sidebar_refactor
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58638
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326111
+milestone: '13.11'
type: development
-group: group::source code
+group: group::editor
default_enabled: false
diff --git a/config/feature_flags/development/usage_data_i_incident_management_oncall_notification_sent.yml b/config/feature_flags/development/usage_data_i_incident_management_oncall_notification_sent.yml
new file mode 100644
index 00000000000..3b8d02cc2d4
--- /dev/null
+++ b/config/feature_flags/development/usage_data_i_incident_management_oncall_notification_sent.yml
@@ -0,0 +1,8 @@
+---
+name: usage_data_i_incident_management_oncall_notification_sent
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58606
+rollout_issue_url:
+milestone: '13.11'
+type: development
+group: group::monitor
+default_enabled: true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 19b43a8c084..7e0c2778e85 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -566,11 +566,11 @@ Settings.cron_jobs['user_status_cleanup_batch_worker']['job_class'] = 'UserStatu
Settings.cron_jobs['ssh_keys_expired_notification_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ssh_keys_expired_notification_worker']['cron'] ||= '0 2 * * *'
Settings.cron_jobs['ssh_keys_expired_notification_worker']['job_class'] = 'SshKeys::ExpiredNotificationWorker'
+Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *'
+Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker'
Gitlab.com do
- Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({})
- Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *'
- Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker'
Settings.cron_jobs['batched_background_migrations_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['batched_background_migrations_worker']['cron'] ||= '* * * * *'
Settings.cron_jobs['batched_background_migrations_worker']['job_class'] = 'Database::BatchedBackgroundMigrationWorker'
diff --git a/db/migrate/20210308175224_change_namespace_settings_delayed_project_removal_null.rb b/db/migrate/20210308175224_change_namespace_settings_delayed_project_removal_null.rb
new file mode 100644
index 00000000000..5b731b78117
--- /dev/null
+++ b/db/migrate/20210308175224_change_namespace_settings_delayed_project_removal_null.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ChangeNamespaceSettingsDelayedProjectRemovalNull < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ change_column :namespace_settings, :delayed_project_removal, :boolean, null: true, default: nil
+ end
+
+ def down
+ change_column_default :namespace_settings, :delayed_project_removal, false
+ change_column_null :namespace_settings, :delayed_project_removal, false, false
+ end
+end
diff --git a/db/migrate/20210308175225_add_lock_delayed_project_removal_to_namespace_settings.rb b/db/migrate/20210308175225_add_lock_delayed_project_removal_to_namespace_settings.rb
new file mode 100644
index 00000000000..e88f3e7ea0d
--- /dev/null
+++ b/db/migrate/20210308175225_add_lock_delayed_project_removal_to_namespace_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddLockDelayedProjectRemovalToNamespaceSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :namespace_settings, :lock_delayed_project_removal, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20210308175226_add_delayed_project_removal_to_application_settings.rb b/db/migrate/20210308175226_add_delayed_project_removal_to_application_settings.rb
new file mode 100644
index 00000000000..1ccb25878e4
--- /dev/null
+++ b/db/migrate/20210308175226_add_delayed_project_removal_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDelayedProjectRemovalToApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :delayed_project_removal, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20210308175227_add_lock_delayed_project_removal_to_application_settings.rb b/db/migrate/20210308175227_add_lock_delayed_project_removal_to_application_settings.rb
new file mode 100644
index 00000000000..c63175493de
--- /dev/null
+++ b/db/migrate/20210308175227_add_lock_delayed_project_removal_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddLockDelayedProjectRemovalToApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :lock_delayed_project_removal, :boolean, default: false, null: false
+ end
+end
diff --git a/db/schema_migrations/20210308175224 b/db/schema_migrations/20210308175224
new file mode 100644
index 00000000000..c222b9101af
--- /dev/null
+++ b/db/schema_migrations/20210308175224
@@ -0,0 +1 @@
+ad6e0feff16589839714098a69673edcba50af7a62d98cd078585c5d2aada919 \ No newline at end of file
diff --git a/db/schema_migrations/20210308175225 b/db/schema_migrations/20210308175225
new file mode 100644
index 00000000000..7fb92d10f8c
--- /dev/null
+++ b/db/schema_migrations/20210308175225
@@ -0,0 +1 @@
+9263c522f0632f5b4fc0004e1fe9666bc3a44e4f70cf0d21aab5bb229f08ab5c \ No newline at end of file
diff --git a/db/schema_migrations/20210308175226 b/db/schema_migrations/20210308175226
new file mode 100644
index 00000000000..4d126ff2b63
--- /dev/null
+++ b/db/schema_migrations/20210308175226
@@ -0,0 +1 @@
+72491b1834a1256a197e8f49c599b28b41773226db4fe70ce402903674d2f622 \ No newline at end of file
diff --git a/db/schema_migrations/20210308175227 b/db/schema_migrations/20210308175227
new file mode 100644
index 00000000000..66aaf4ca558
--- /dev/null
+++ b/db/schema_migrations/20210308175227
@@ -0,0 +1 @@
+e99b8a6242589992ae8b618cb502d16b67672856cef024c1aafe00a1e64e41b9 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e8a434471e9..1dd216b52da 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9439,6 +9439,8 @@ CREATE TABLE application_settings (
in_product_marketing_emails_enabled boolean DEFAULT true NOT NULL,
asset_proxy_whitelist text,
admin_mode boolean DEFAULT false NOT NULL,
+ delayed_project_removal boolean DEFAULT false NOT NULL,
+ lock_delayed_project_removal boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)),
@@ -14665,8 +14667,9 @@ CREATE TABLE namespace_settings (
allow_mfa_for_subgroups boolean DEFAULT true NOT NULL,
default_branch_name text,
repository_read_only boolean DEFAULT false NOT NULL,
- delayed_project_removal boolean DEFAULT false NOT NULL,
+ delayed_project_removal boolean,
resource_access_token_creation_allowed boolean DEFAULT true NOT NULL,
+ lock_delayed_project_removal boolean DEFAULT false NOT NULL,
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255))
);
diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md
index d07afb3bb14..88e3369e25e 100644
--- a/doc/administration/operations/extra_sidekiq_processes.md
+++ b/doc/administration/operations/extra_sidekiq_processes.md
@@ -75,7 +75,7 @@ To start multiple processes:
When `sidekiq-cluster` is only running on a single node, make sure that at least
one process is running on all queues using `*`. This means a process will
- automatically pick up jobs in queues created in the future.
+ This includes queues that have dedicated processes.
If `sidekiq-cluster` is running on more than one node, you can also use
[`--negate`](#negate-settings) and list all the queues that are already being
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 69eb639a8db..2506d4cbf0e 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -215,8 +215,8 @@ project = Project.find_by_full_path('group-changeme/project-changeme')
### Destroy a project
```ruby
-project = Project.find_by_full_path('')
-user = User.find_by_username('')
+project = Project.find_by_full_path('<project_path>')
+user = User.find_by_username('<username>')
ProjectDestroyWorker.perform_async(project.id, user.id, {})
# or ProjectDestroyWorker.new.perform(project.id, user.id, {})
# or Projects::DestroyService.new(project, user).execute
@@ -225,8 +225,8 @@ ProjectDestroyWorker.perform_async(project.id, user.id, {})
### Remove fork relationship manually
```ruby
-p = Project.find_by_full_path('')
-u = User.find_by_username('')
+p = Project.find_by_full_path('<project_path>')
+u = User.find_by_username('<username>')
::Projects::UnlinkForkService.new(p, u).execute
```
@@ -243,13 +243,13 @@ project.update!(repository_read_only: true)
### Transfer project from one namespace to another
```ruby
- p= Project.find_by_full_path('')
+ p= Project.find_by_full_path('<project_path>')
# To set the owner of the project
current_user= p.creator
# Namespace where you want this to be moved.
-namespace = Namespace.find_by_full_path("")
+namespace = Namespace.find_by_full_path("<new_namespace>")
::Projects::TransferService.new(p, current_user).execute(namespace)
```
@@ -468,7 +468,7 @@ end
### Skip reconfirmation
```ruby
-user = User.find_by_username ''
+user = User.find_by_username '<username>'
user.skip_reconfirmation!
```
@@ -558,7 +558,7 @@ user.max_member_access_for_group group.id
```ruby
user = User.find_by_username('<username>')
group = Group.find_by_name("<group_name>")
-parent_group = Group.find_by(id: "") # empty string amounts to root as parent
+parent_group = Group.find_by(id: "<group_id>")
service = ::Groups::TransferService.new(group, user)
service.execute(parent_group)
```
@@ -679,7 +679,7 @@ conflicting_permanent_redirects.destroy_all
```ruby
p = Project.find_by_full_path('<full/path/to/project>')
m = p.merge_requests.find_by(iid: <iid>)
-u = User.find_by_username('')
+u = User.find_by_username('<username>')
MergeRequests::PostMergeService.new(p, u).execute(m)
```
@@ -695,9 +695,9 @@ Issuable::DestroyService.new(m.project, u).execute(m)
### Rebase manually
```ruby
-p = Project.find_by_full_path('')
+p = Project.find_by_full_path('<project_path>')
m = project.merge_requests.find_by(iid: )
-u = User.find_by_username('')
+u = User.find_by_username('<username>')
MergeRequests::RebaseService.new(m.target_project, u).execute(m)
```
@@ -734,7 +734,7 @@ build.dependencies.each do |d| { puts "status: #{d.status}, finished at: #{d.fin
### Try CI service
```ruby
-p = Project.find_by_full_path('')
+p = Project.find_by_full_path('<project_path>')
m = project.merge_requests.find_by(iid: )
m.project.try(:ci_service)
```
diff --git a/doc/api/users.md b/doc/api/users.md
index a613add10bf..4c35ee0e531 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -346,6 +346,7 @@ Example Responses:
"two_factor_enabled": true,
"external": false,
"private_profile": false,
+ "commit_email": "john-codes@example.com",
"current_sign_in_ip": "196.165.1.102",
"last_sign_in_ip": "172.127.2.22",
"plan": "gold",
@@ -440,7 +441,6 @@ Parameters:
| `private_profile` | No | User's profile is private - true, false (default), or null (is converted to false) |
| `projects_limit` | No | Number of projects user can create |
| `provider` | No | External provider name |
-| `public_email` | No | The public email of the user |
| `reset_password` | No | Send user password reset link - true or false(default) |
| `shared_runners_minutes_limit` | No | Pipeline minutes quota for this user (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` **(STARTER)** |
| `skip_confirmation` | No | Skip confirmation - true or false (default) |
@@ -483,7 +483,7 @@ Parameters:
| `private_profile` | No | User's profile is private - true, false (default), or null (is converted to false) |
| `projects_limit` | No | Limit projects each user can create |
| `provider` | No | External provider name |
-| `public_email` | No | The public email of the user |
+| `public_email` | No | The public email of the user (must be already verified) |
| `shared_runners_minutes_limit` | No | Pipeline minutes quota for this user (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` **(STARTER)** |
| `skip_reconfirmation` | No | Skip reconfirmation - true or false (default) |
| `skype` | No | Skype ID |
@@ -622,6 +622,7 @@ GET /user
"two_factor_enabled": true,
"external": false,
"private_profile": false,
+ "commit_email": "john-codes@example.com",
"current_sign_in_ip": "196.165.1.102",
"last_sign_in_ip": "172.127.2.22"
}
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index ed2e7c132de..76d44715e8a 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -125,25 +125,24 @@ For more information about the `environment` keywords, see
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/300741) in GitLab 13.10.
-There are cases where you might want to use a code name as an environment name instead of using
-an [industry standard](https://en.wikipedia.org/wiki/Deployment_environment). For example, your environment might be called `customer-portal` instead of `production`.
-This is perfectly fine, however, it loses information that the specific
-environment is used as production.
-
-To keep information that a specific environment is for production or
-some other use, you can set one of the following tiers to each environment:
-
-| Environment tier | Environment names examples |
-| ---- | -------- |
-| `production` | Production, Live |
-| `staging` | Staging, Model, Pre, Demo |
-| `testing` | Test, QC |
-| `development` | Dev, [Review apps](../review_apps/index.md), Trunk |
-| `other` | |
-
-By default, an approximate tier is automatically guessed and set from [the environment name](../yaml/README.md#environmentname).
-Alternatively, you can specify a specific tier with `deployment_tier` keyword,
-see the [`.gitlab-ci.yml` syntax reference](../yaml/README.md#environmentdeployment_tier) for more details.
+Sometimes, instead of using an [industry standard](https://en.wikipedia.org/wiki/Deployment_environment)
+environment name, like `production`, you might want to use a code name, like `customer-portal`.
+While there is no technical reason not to use a name like `customer-portal`, the name
+no longer indicates that the environment is used for production.
+
+To indicate that a specific environment is for a specific use,
+you can use tiers:
+
+| Environment tier | Environment name examples |
+|------------------|----------------------------------------------------|
+| `production` | Production, Live |
+| `staging` | Staging, Model, Pre, Demo |
+| `testing` | Test, QC |
+| `development` | Dev, [Review apps](../review_apps/index.md), Trunk |
+| `other` | |
+
+By default, GitLab assumes a tier based on [the environment name](../yaml/README.md#environmentname).
+Instead, you can use the [`deployment_tier` keyword](../yaml/README.md#environmentdeployment_tier) to specify a tier.
## Configure manual deployments
@@ -206,8 +205,8 @@ deploy:
```
When you use the GitLab Kubernetes integration to deploy to a Kubernetes cluster,
-cluster and namespace information is displayed above the job
-trace on the deployment job page:
+you can view cluster and namespace information. On the deployment
+job page, it's displayed above the job trace:
![Deployment cluster information](../img/environments_deployment_cluster_v12_8.png)
@@ -259,7 +258,7 @@ For an overview, see [Set dynamic URLs after a job finished](https://youtu.be/70
### Example of setting dynamic environment URLs
The following example shows a Review App that creates a new environment
-per merge request. The `review` job is triggered by every push, and
+for each merge request. The `review` job is triggered by every push, and
creates or updates an environment named `review/your-branch-name`.
The environment URL is set to `$DYNAMIC_ENVIRONMENT_URL`:
@@ -347,7 +346,7 @@ places in GitLab:
You can see this information in a merge request if:
-- The merge request is eventually merged to the default branch (usually `master`).
+- The merge request is eventually merged to the default branch (usually `main`).
- That branch also deploys to an environment (for example, `staging` or `production`).
For example:
@@ -430,7 +429,7 @@ Due to resource limitations, a background worker for stopping environments only
every hour. This means that environments aren't stopped at the exact timestamp specified, but are
instead stopped when the hourly cron worker detects expired environments.
-In the following example, each merge request creates a new Review App environment.
+In the following example, each merge request creates a Review App environment.
Each push triggers the `review_app` job and an environment named `review/your-branch-name`
is created or updated. The environment runs until `stop_review_app` is executed:
@@ -477,7 +476,7 @@ You can manually override a deployment's expiration date.
1. Go to the project's **Operations > Environments** page.
1. Select the deployment name.
-1. In the top right, select the thumbtack (**{thumbtack}**).
+1. On the top right, select the thumbtack (**{thumbtack}**).
![Environment auto stop](img/environment_auto_stop_v13_10.png)
@@ -501,7 +500,7 @@ To delete a stopped environment in the GitLab UI:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208655) in GitLab 13.2.
-By default, whenever GitLab CI/CD runs a job for a specific environment, it
+By default, when GitLab CI/CD runs a job for a specific environment, it
triggers a deployment and [(optionally) cancels outdated
deployments](deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time).
@@ -525,8 +524,6 @@ and can be used to [protect builds from unauthorized access](protected_environme
### Group similar environments
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7015) in GitLab 8.14.
-
You can group environments into collapsible sections in the UI.
For example, if all of your environments start with the name `review`,
@@ -548,10 +545,9 @@ deploy_review:
### Environment incident management
-You have successfully setup a Continuous Delivery/Deployment workflow in your project.
Production environments can go down unexpectedly, including for reasons outside
-of your own control. For example, issues with external dependencies, infrastructure,
-or human error can cause major issues with an environment. This could include:
+of your control. For example, issues with external dependencies, infrastructure,
+or human error can cause major issues with an environment. Things like:
- A dependent cloud service goes down.
- A 3rd party library is updated and it's not compatible with your application.
@@ -573,7 +569,7 @@ severity is shown, so you can identify which environments need immediate attenti
![Environment alert](img/alert_for_environment.png)
When the issue that triggered the alert is resolved, it is removed and is no
-longer visible on the environment page.
+longer visible on the environments page.
If the alert requires a [rollback](#retry-or-roll-back-a-deployment), you can select the
deployment tab from the environment page and select which deployment to roll back to.
@@ -585,7 +581,7 @@ deployment tab from the environment page and select which deployment to roll bac
In a typical Continuous Deployment workflow, the CI pipeline tests every commit before deploying to
production. However, problematic code can still make it to production. For example, inefficient code
that is logically correct can pass tests even though it causes severe performance degradation.
-Operators and SREs monitor the system to catch such problems as soon as possible. If they find a
+Operators and SREs monitor the system to catch these problems as soon as possible. If they find a
problematic deployment, they can roll back to a previous stable version.
GitLab Auto Rollback eases this workflow by automatically triggering a rollback when a
@@ -600,54 +596,49 @@ Limitations of GitLab Auto Rollback:
GitLab Auto Rollback is turned off by default. To turn it on:
-1. Visit **Project > Settings > CI/CD > Automatic deployment rollbacks**.
+1. Go to **Project > Settings > CI/CD > Automatic deployment rollbacks**.
1. Select the checkbox for **Enable automatic rollbacks**.
-1. Click **Save changes**.
+1. Select **Save changes**.
### Monitoring environments
-If you have enabled [Prometheus for monitoring system and response metrics](../../user/project/integrations/prometheus.md),
-you can monitor the behavior of your app running in each environment. For the monitoring
-dashboard to appear, you need to Configure Prometheus to collect at least one
+To monitor the behavior of your app as it runs in each environment,
+enable [Prometheus for monitoring system and response metrics](../../user/project/integrations/prometheus.md).
+For the monitoring dashboard to appear, configure Prometheus to collect at least one
[supported metric](../../user/project/integrations/prometheus_library/index.md).
-In GitLab 9.2 and later, all deployments to an environment are shown directly on the monitoring dashboard.
+All deployments to an environment are shown on the monitoring dashboard.
+You can view changes in performance for each version of your application.
-Once configured, GitLab attempts to retrieve [supported performance metrics](../../user/project/integrations/prometheus_library/index.md)
+GitLab attempts to retrieve [supported performance metrics](../../user/project/integrations/prometheus_library/index.md)
for any environment that has had a successful deployment. If monitoring data was
successfully retrieved, a **Monitoring** button appears for each environment.
-Clicking the **Monitoring** button displays a new page showing up to the last
-8 hours of performance data. It may take a minute or two for data to appear
-after initial deployment.
-
-All deployments to an environment are shown directly on the monitoring dashboard,
-which allows easy correlation between any changes in performance and new
-versions of the app, all without leaving GitLab.
+To view the last eight hours of performance data, select the **Monitoring** button.
+It may take a minute or two for data to appear after initial deployment.
![Monitoring dashboard](../img/environments_monitoring.png)
#### Embedding metrics in GitLab Flavored Markdown
-Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab Flavored Markdown](../../operations/metrics/embed.md) for more details.
+Metric charts can be embedded in GitLab Flavored Markdown. See [Embedding Metrics in GitLab Flavored Markdown](../../operations/metrics/embed.md) for more details.
### Web terminals
-> Web terminals were added in GitLab 8.15 and are only available to project Maintainers and Owners.
-
If you deploy to your environments with the help of a deployment service (for example,
the [Kubernetes integration](../../user/project/clusters/index.md)), GitLab can open
-a terminal session to your environment.
+a terminal session to your environment. You can then debug issues without leaving your web browser.
+
+The Web terminal is a container-based deployment, which often lack basic tools (like an editor),
+and can be stopped or restarted at any time. If this happens, you lose all your
+changes. Treat the Web terminal as a debugging tool, not a comprehensive online IDE.
-This is a powerful feature that allows you to debug issues without leaving the comfort
-of your web browser. To enable it, follow the instructions given in the service integration
-documentation.
+Web terminals:
-Note that container-based deployments often lack basic tools (like an editor), and may
-be stopped or restarted at any time. If this happens, you lose all your
-changes. Treat this as a debugging tool, not a comprehensive online IDE.
+- Are available to project Maintainers and Owners only.
+- Must [be enabled](../../administration/integration/terminal.md).
-Once enabled, your environments display a **Terminal** button:
+In the UI, you can view the Web terminal by selecting a **Terminal** button:
![Terminal button on environment index](img/environments_terminal_button_on_index_v13_10.png)
@@ -655,8 +646,7 @@ You can also access the terminal button from the page for a specific environment
![Terminal button for an environment](img/environments_terminal_button_on_show_v13_10.png)
-Wherever you find it, clicking the button takes you to a separate page to
-establish the terminal session:
+Select the button to establish the terminal session:
![Terminal page](../img/environments_terminal_page.png)
@@ -665,14 +655,14 @@ by your deployment so you can:
- Run shell commands and get responses in real time.
- Check the logs.
-- Try out configuration or code tweaks etc.
+- Try out configuration or code tweaks.
-You can open multiple terminals to the same environment, they each get their own shell
+You can open multiple terminals to the same environment. They each get their own shell
session and even a multiplexer like `screen` or `tmux`.
### Check out deployments locally
-In GitLab 8.13 and later, a reference in the Git repository is saved for each deployment, so
+A reference in the Git repository is saved for each deployment, so
knowing the state of your current environments is only a `git fetch` away.
In your Git configuration, append the `[remote "<your-remote>"]` block with an extra
@@ -689,24 +679,23 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
You can limit the environment scope of a CI/CD variable by
defining which environments it can be available for.
+For example, if the environment scope is `production`, then only the jobs
+with the environment `production` defined would have this specific variable.
-Wildcards can be used and the default environment scope is `*`. This means that
-any jobs can have this variable regardless of whether an environment is defined.
+The default environment scope is a wildcard (`*`), which means that
+any job can have this variable, regardless of whether an environment is defined.
-For example, if the environment scope is `production`, then only the jobs
-having the environment `production` defined would have this specific variable.
-Wildcards (`*`) can be used along with the environment name, therefore if the
-environment scope is `review/*` then any jobs with environment names starting
-with `review/` would have that particular variable.
+If the environment scope is `review/*`, then jobs with environment names starting
+with `review/` would have that variable available.
Some GitLab features can behave differently for each environment.
For example, you can
[create a secret variable to be injected only into a production environment](../variables/README.md#limit-the-environment-scopes-of-cicd-variables).
In most cases, these features use the _environment specs_ mechanism, which offers
-an efficient way to implement scoping within each environment group.
+an efficient way to implement scoping in each environment group.
-Let's say there are four environments:
+For example, if there are four environments:
- `production`
- `staging`
@@ -723,11 +712,11 @@ Each environment can be matched with the following environment spec:
| review/* | | | Matched | Matched |
| review/feature-1 | | | Matched | |
-As you can see, you can use specific matching for selecting a particular environment,
-and also use wildcard matching (`*`) for selecting a particular environment group,
-such as [Review Apps](../review_apps/index.md) (`review/*`).
+You can use specific matching to select a particular environment.
+You can also use wildcard matching (`*`) to select a particular environment group,
+like [Review Apps](../review_apps/index.md) (`review/*`).
-Note that the most _specific_ spec takes precedence over the other wildcard matching. In this case,
+The most specific spec takes precedence over the other wildcard matching. In this case,
the `review/feature-1` spec takes precedence over `review/*` and `*` specs.
## Related topics
diff --git a/doc/development/fe_guide/accessibility.md b/doc/development/fe_guide/accessibility.md
index 87092b76c2f..ab1325c67a9 100644
--- a/doc/development/fe_guide/accessibility.md
+++ b/doc/development/fe_guide/accessibility.md
@@ -15,39 +15,264 @@ This page contains guidelines we should follow.
Since [no ARIA is better than bad ARIA](https://www.w3.org/TR/wai-aria-practices/#no_aria_better_bad_aria),
review the following recommendations before using `aria-*`, `role`, and `tabindex`.
-Use semantic HTML, which typically has accessibility semantics baked in, but always be sure to test with
+Use semantic HTML, which has accessibility semantics baked in, and ideally test with
[relevant combinations of screen readers and browsers](https://www.accessibility-developer-guide.com/knowledge/screen-readers/relevant-combinations/).
In [WebAIM's accessibility analysis of the top million home pages](https://webaim.org/projects/million/#aria),
they found that "ARIA correlated to higher detectable errors".
It is likely that *misuse* of ARIA is a big cause of increased errors,
-so when in doubt don't use `aria-*`, `role`, and `tabindex`, and stick with semantic HTML.
-
-## Provide accessible names to screen readers
+so when in doubt don't use `aria-*`, `role`, and `tabindex` and stick with semantic HTML.
+
+## Quick checklist
+
+- [Text](#text-inputs-with-accessible-names),
+ [select](#select-inputs-with-accessible-names),
+ [checkbox](#checkbox-inputs-with-accessible-names),
+ [radio](#radio-inputs-with-accessible-names),
+ [file](#file-inputs-with-accessible-names),
+ and [toggle](#gltoggle-components-with-an-accessible-names) inputs have accessible names.
+- [Buttons](#buttons-and-links-with-descriptive-accessible-names),
+ [links](#buttons-and-links-with-descriptive-accessible-names),
+ and [images](#images-with-accessible-names) have descriptive accessible names.
+- Icons
+ - [Non-decorative icons](#icons-that-convey-information) have an `aria-label`.
+ - [Clickable icons](#icons-that-are-clickable) are buttons, that is, `<gl-button icon="close" />` is used and not `<gl-icon />`.
+ - Icon-only buttons have an `aria-label`.
+- Interactive elements can be [accessed with the Tab key](#support-keyboard-only-use) and have a visible focus state.
+- Are any `role`, `tabindex` or `aria-*` attributes unnecessary?
+- Can any `div` or `span` elements be replaced with a more semantic [HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element) like `p`, `button`, or `time`?
+
+## Provide accessible names for screen readers
To provide markup with accessible names, ensure every:
- `input` has an associated `label`.
-- `button` and `a` have child text, or `aria-label` when text isn't present.
- For example, an icon button with no visible text.
+- `button` and `a` have child text, or `aria-label` when child text isn't present, such as for an icon button with no content.
- `img` has an `alt` attribute.
- `fieldset` has `legend` as its first child.
- `figure` has `figcaption` as its first child.
- `table` has `caption` as its first child.
+Groups of checkboxes and radio inputs should be grouped together in a `fieldset` with a `legend`.
+`legend` gives the group of checkboxes and radio inputs a label.
+
If the `label`, child text, or child element is not visually desired,
use `.gl-sr-only` to hide the element from everything but screen readers.
-Ensure the accessible name is descriptive enough to be understood in isolation.
+### Examples of providing accessible names
+
+The following subsections contain examples of markup that render HTML elements with accessible names.
+
+Note that [when using `GlFormGroup`](https://bootstrap-vue.org/docs/components/form-group#accessibility):
+
+- Passing only a `label` prop renders a `fieldset` with a `legend` containing the `label` value.
+- Passing both a `label` and a `label-for` prop renders a `label` that points to the form input with the same `label-for` ID.
+
+#### Text inputs with accessible names
+
+When using `GlFormGroup`, the `label` prop alone does not give the input an accessible name.
+The `label-for` prop must also be provided to give the input an accessible name.
+
+Text input examples:
+
+```html
+<!-- Input with label -->
+<gl-form-group :label="__('Issue title')" label-for="issue-title">
+ <gl-form-input id="issue-title" v-model="title" name="title" />
+</gl-form-group>
+
+<!-- Input with hidden label -->
+<gl-form-group :label="__('Issue title')" label-for="issue-title" :label-sr-only="true">
+ <gl-form-input id="issue-title" v-model="title" name="title" />
+</gl-form-group>
+```
+
+Textarea examples:
+
+```html
+<!-- Textarea with label -->
+<gl-form-group :label="__('Issue description')" label-for="issue-description">
+ <gl-form-textarea id="issue-description" v-model="description" name="description" />
+</gl-form-group>
+
+<!-- Textarea with hidden label -->
+<gl-form-group :label="__('Issue description')" label-for="issue-description" :label-sr-only="true">
+ <gl-form-textarea id="issue-description" v-model="description" name="description" />
+</gl-form-group>
+```
+
+Alternatively, you can use a plain `label` element:
+
+```html
+<!-- Input with label using `label` -->
+<label for="issue-title">{{ __('Issue title') }}</label>
+<gl-form-input id="issue-title" v-model="title" name="title" />
+
+<!-- Input with hidden label using `label` -->
+<label for="issue-title" class="gl-sr-only">{{ __('Issue title') }}</label>
+<gl-form-input id="issue-title" v-model="title" name="title" />
+```
+
+#### Select inputs with accessible names
+
+Select input examples:
+
+```html
+<!-- Select input with label -->
+<gl-form-group :label="__('Issue status')" label-for="issue-status">
+ <gl-form-select id="issue-status" v-model="status" name="status" :options="options" />
+</gl-form-group>
+
+<!-- Select input with hidden label -->
+<gl-form-group :label="__('Issue status')" label-for="issue-status" :label-sr-only="true">
+ <gl-form-select id="issue-status" v-model="status" name="status" :options="options" />
+</gl-form-group>
+```
+
+#### Checkbox inputs with accessible names
+
+Single checkbox:
+
+```html
+<!-- Single checkbox with label -->
+<gl-form-checkbox v-model="status" name="status" value="task-complete">
+ {{ __('Task complete') }}
+</gl-form-checkbox>
+
+<!-- Single checkbox with hidden label -->
+<gl-form-checkbox v-model="status" name="status" value="task-complete">
+ <span class="gl-sr-only">{{ __('Task complete') }}</span>
+</gl-form-checkbox>
+```
+
+Multiple checkboxes:
+
+```html
+<!-- Multiple labeled checkboxes grouped within a fieldset -->
+<gl-form-group :label="__('Task list')">
+ <gl-form-checkbox name="task-list" value="task-1">{{ __('Task 1') }}</gl-form-checkbox>
+ <gl-form-checkbox name="task-list" value="task-2">{{ __('Task 2') }}</gl-form-checkbox>
+</gl-form-group>
+
+<!-- Or -->
+<gl-form-group :label="__('Task list')">
+ <gl-form-checkbox-group v-model="selected" :options="options" name="task-list" />
+</gl-form-group>
+
+<!-- Multiple labeled checkboxes grouped within a fieldset with hidden legend -->
+<gl-form-group :label="__('Task list')" :label-sr-only="true">
+ <gl-form-checkbox name="task-list" value="task-1">{{ __('Task 1') }}</gl-form-checkbox>
+ <gl-form-checkbox name="task-list" value="task-2">{{ __('Task 2') }}</gl-form-checkbox>
+</gl-form-group>
+
+<!-- Or -->
+<gl-form-group :label="__('Task list')" :label-sr-only="true">
+ <gl-form-checkbox-group v-model="selected" :options="options" name="task-list" />
+</gl-form-group>
+```
+
+#### Radio inputs with accessible names
+
+Single radio input:
+
+```html
+<!-- Single radio with a label -->
+<gl-form-radio v-model="status" name="status" value="opened">
+ {{ __('Opened') }}
+</gl-form-radio>
+
+<!-- Single radio with a hidden label -->
+<gl-form-radio v-model="status" name="status" value="opened">
+ <span class="gl-sr-only">{{ __('Opened') }}</span>
+</gl-form-radio>
+```
+
+Multiple radio inputs:
+
+```html
+<!-- Multiple labeled radio inputs grouped within a fieldset -->
+<gl-form-group :label="__('Issue status')">
+ <gl-form-radio name="status" value="opened">{{ __('Opened') }}</gl-form-radio>
+ <gl-form-radio name="status" value="closed">{{ __('Closed') }}</gl-form-radio>
+</gl-form-group>
+
+<!-- Or -->
+<gl-form-group :label="__('Issue status')">
+ <gl-form-radio-group v-model="selected" :options="options" name="status" />
+</gl-form-group>
+
+<!-- Multiple labeled radio inputs grouped within a fieldset with hidden legend -->
+<gl-form-group :label="__('Issue status')" :label-sr-only="true">
+ <gl-form-radio name="status" value="opened">{{ __('Opened') }}</gl-form-radio>
+ <gl-form-radio name="status" value="closed">{{ __('Closed') }}</gl-form-radio>
+</gl-form-group>
+
+<!-- Or -->
+<gl-form-group :label="__('Issue status')" :label-sr-only="true">
+ <gl-form-radio-group v-model="selected" :options="options" name="status" />
+</gl-form-group>
+```
+
+#### File inputs with accessible names
+
+File input examples:
+
+```html
+<!-- File input with a label -->
+<label for="attach-file">{{ __('Attach a file') }}</label>
+<input id="attach-file" type="file" name="attach-file" />
+
+<!-- File input with a hidden label -->
+<label for="attach-file" class="gl-sr-only">{{ __('Attach a file') }}</label>
+<input id="attach-file" type="file" name="attach-file" />
+```
+
+#### GlToggle components with an accessible names
+
+`GlToggle` examples:
```html
-// bad
-<button>Submit</button>
-<a href="url">page</a>
+<!-- GlToggle with label -->
+<gl-toggle v-model="notifications" :label="__('Notifications')" />
-// good
-<button>Submit review</button>
-<a href="url">GitLab's accessibility page</a>
+<!-- GlToggle with hidden label -->
+<gl-toggle v-model="notifications" :label="__('Notifications')" label-position="hidden" />
+```
+
+#### GlFormCombobox components with an accessible names
+
+`GlFormCombobox` examples:
+
+```html
+<!-- GlFormCombobox with label -->
+<gl-form-combobox :label-text="__('Key')" :token-list="$options.tokenList" />
+```
+
+#### Images with accessible names
+
+Image examples:
+
+```html
+<img :src="imagePath" :alt="__('A description of the image')" />
+
+<!-- SVGs implicitly have a graphics role so if it is semantically an image we should apply `role="img"` -->
+<svg role="img" :alt="__('A description of the image')" />
+```
+
+#### Buttons and links with descriptive accessible names
+
+Buttons and links should have accessible names that are descriptive enough to be understood in isolation.
+
+```html
+<!-- bad -->
+<gl-button @click="handleClick">{{ __('Submit') }}</gl-button>
+
+<gl-link :href="url">{{ __('page') }}</gl-link>
+
+<!-- good -->
+<gl-button @click="handleClick">{{ __('Submit review') }}</gl-button>
+
+<gl-link :href="url">{{ __("GitLab's accessibility page") }}</gl-link>
```
## Role
@@ -81,31 +306,37 @@ element is interactive you must ensure:
Use semantic HTML, such as `a` and `button`, which provides these behaviours by default.
+Keep in mind that:
+
+- <kbd>Tab</kbd> and <kbd>Shift-Tab</kbd> should only move between interactive elements, not static content.
+- When you add `:hover` styles, in most cases you should add `:focus` styles too so that the styling is applied for both mouse **and** keyboard users.
+- If you remove an interactive element's `outline`, make sure you maintain visual focus state in another way such as with `box-shadow`.
+
See the [Pajamas Keyboard-only page](https://design.gitlab.com/accessibility-audits/2-keyboard-only/) for more detail.
## Tabindex
Prefer **no** `tabindex` to using `tabindex`, since:
-- Using semantic HTML such as `button` implicitly provides `tabindex="0"`
-- Tabbing order should match the visual reading order and positive `tabindex`s interfere with this
+- Using semantic HTML such as `button` implicitly provides `tabindex="0"`.
+- Tabbing order should match the visual reading order and positive `tabindex`s interfere with this.
### Avoid using `tabindex="0"` to make an element interactive
-Use interactive elements instead of `div`s and `span`s.
+Use interactive elements instead of `div` and `span` tags.
For example:
-- If the element should be clickable, use a `button`
-- If the element should be text editable, use an `input` or `textarea`
+- If the element should be clickable, use a `button`.
+- If the element should be text editable, use an `input` or `textarea`.
Once the markup is semantically complete, use CSS to update it to its desired visual state.
```html
-// bad
+<!-- bad -->
<div role="button" tabindex="0" @click="expand">Expand</div>
-// good
-<button @click="expand">Expand</button>
+<!-- good -->
+<gl-button @click="expand">Expand</gl-button>
```
### Do not use `tabindex="0"` on interactive elements
@@ -113,13 +344,13 @@ Once the markup is semantically complete, use CSS to update it to its desired vi
Interactive elements are already tab accessible so adding `tabindex` is redundant.
```html
-// bad
-<a href="help" tabindex="0">Help</a>
-<button tabindex="0">Submit</button>
+<!-- bad -->
+<gl-link href="help" tabindex="0">Help</gl-link>
+<gl-button tabindex="0">Submit</gl-button>
-// good
-<a href="help">Help</a>
-<button>Submit</button>
+<!-- good -->
+<gl-link href="help">Help</gl-link>
+<gl-button>Submit</gl-button>
```
### Do not use `tabindex="0"` on elements for screen readers to read
@@ -129,10 +360,10 @@ The use of `tabindex="0"` is unnecessary and can cause problems,
as screen reader users then expect to be able to interact with it.
```html
-// bad
-<span tabindex="0" :aria-label="message">{{ message }}</span>
+<!-- bad -->
+<p tabindex="0" :aria-label="message">{{ message }}</p>
-// good
+<!-- good -->
<p>{{ message }}</p>
```
@@ -141,6 +372,57 @@ as screen reader users then expect to be able to interact with it.
[Always avoid using `tabindex="1"`](https://webaim.org/techniques/keyboard/tabindex#overview)
or greater.
+## Icons
+
+Icons can be split into three different types:
+
+- Icons that are decorative
+- Icons that convey meaning
+- Icons that are clickable
+
+### Icons that are decorative
+
+Icons are decorative when there's no loss of information to the user when they are removed from the UI.
+
+As the majority of icons within GitLab are decorative, `GlIcon` automatically hides its rendered icons from screen readers.
+Therefore, you do not need to add `aria-hidden="true"` to `GlIcon`, as this is redundant.
+
+```html
+<!-- unnecessary — gl-icon hides icons from screen readers by default -->
+<gl-icon name="rocket" aria-hidden="true" />`
+
+<!-- good -->
+<gl-icon name="rocket" />`
+```
+
+### Icons that convey information
+
+Icons convey information if there is loss of information to the user when they are removed from the UI.
+
+An example is a confidential icon that conveys the issue is confidential, and does not have the text "Confidential" next to it.
+
+Icons that convey information must have an accessible name so that the information is conveyed to screen reader users too.
+
+```html
+<!-- bad -->
+<gl-icon name="eye-slash" />`
+
+<!-- good -->
+<gl-icon name="eye-slash" :aria-label="__('Confidential issue')" />`
+```
+
+### Icons that are clickable
+
+Icons that are clickable are semantically buttons, so they should be rendered as buttons, with an accessible name.
+
+```html
+<!-- bad -->
+<gl-icon name="close" :aria-label="__('Close')" @click="handleClick" />
+
+<!-- good -->
+<gl-button icon="close" category="tertiary" :aria-label="__('Close')" @click="handleClick" />
+```
+
## Hiding elements
Use the following table to hide elements from users, when appropriate.
@@ -158,22 +440,24 @@ If the image is not an `img` element, such as an inline SVG, you can hide it by
unnecessary when using `gl-icon`.
```html
-// good - decorative images hidden from screen readers
+<!-- good - decorative images hidden from screen readers -->
+
<img src="decorative.jpg" alt="">
-<svg role="img" alt="">
-<gl-icon name="epic"/>
+
+<svg role="img" alt="" />
+
+<gl-icon name="epic" />
```
-## When should ARIA be used
+## When to use ARIA
-No ARIA is required when using semantic HTML because it incorporates accessibility.
+No ARIA is required when using semantic HTML, because it already incorporates accessibility.
-However, there are some UI patterns and widgets that do not have semantic HTML equivalents.
+However, there are some UI patterns that do not have semantic HTML equivalents.
+General examples of these are dialogs (modals) and tabs.
+GitLab-specific examples are assignee and label dropdowns.
Building such widgets require ARIA to make them understandable to screen readers.
-Proper research and testing should be done to ensure compliance with ARIA.
-
-Ideally, these widgets would exist only in [GitLab UI](https://gitlab-org.gitlab.io/gitlab-ui/).
-Use of ARIA would then only occur in [GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/) and not [GitLab](https://gitlab.com/gitlab-org/gitlab/).
+Proper research and testing should be done to ensure compliance with [WCAG](https://www.w3.org/WAI/standards-guidelines/wcag/).
## Resources
diff --git a/doc/development/permissions.md b/doc/development/permissions.md
index 35f0941b756..2af451840d6 100644
--- a/doc/development/permissions.md
+++ b/doc/development/permissions.md
@@ -120,3 +120,31 @@ into different features like Merge Requests and CI flow.
| View | Vulnerability feedback | Merge Request | Can read security findings |
| View | Dependency List page | Project | Can access Dependency information |
| View | License Compliance page | Project | Can access License information|
+
+## Where should permissions be checked?
+
+By default, controllers, API endpoints, and GraphQL types/fields are responsible for authorization. See [Secure Coding Guidelines > Permissions](secure_coding_guidelines.md#permissions).
+
+### Considerations
+
+- Many actions are completely or partially extracted to services, finders, and other classes, so it is normal to do permission checks "downstream".
+- Often, authorization logic must be incorporated in DB queries to filter records.
+- `DeclarativePolicy` rules are relatively performant, but conditions may perform database calls.
+- Multiple permission checks across layers can be difficult to reason about, which is its own security risk. For example, duplicate authorization logic could diverge.
+- Should we apply defense-in-depth with permission checks? [Join the discussion](https://gitlab.com/gitlab-org/gitlab/-/issues/324135)
+
+### Tips
+
+If a class accepts `current_user`, then it may be responsible for authorization.
+
+### Example: Adding a new API endpoint
+
+By default, we authorize at the endpoint. Checking an existing ability may make sense; if not, then we probably need to add one.
+
+As an aside, most endpoints can be cleanly categorized as a CRUD (create, read, update, destroy) action on a resource. The services and abilities follow suit, which is why many are named like `Projects::CreateService` or `:read_project`.
+
+Say, for example, we extract the whole endpoint into a service. The `can?` check will now be in the service. Say the service reuses an existing finder, which we are modifying for our purposes. Should we make the finder check an ability?
+
+- If the finder doesn't accept `current_user`, and therefore doesn't check permissions, then probably no.
+- If the finder accepts `current_user`, and doesn't check permissions, then it would be a good idea to double check other usages of the finder, and we might consider adding authorization.
+- If the finder accepts `current_user`, and already checks permissions, then either we need to add our case, or the existing checks are appropriate.
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index e2856c0976d..a17e27c1465 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -10412,6 +10412,30 @@ Status: `data_available`
Tiers:
+### `redis_hll_counters.incident_management.i_incident_management_oncall_notification_sent_monthly`
+
+Count of unique users to receive a notification while on-call
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210405222005_i_incident_management_oncall_notification_sent_monthly.yml)
+
+Group: `group::monitor`
+
+Status: `implemented`
+
+Tiers: `premium`, `ultimate`
+
+### `redis_hll_counters.incident_management.i_incident_management_oncall_notification_sent_weekly`
+
+Count of unique users to receive a notification while on-call
+
+[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210405220139_i_incident_management_oncall_notification_sent_weekly.yml)
+
+Group: `group::monitor`
+
+Status: `implemented`
+
+Tiers: `premium`, `ultimate`
+
### `redis_hll_counters.incident_management.incident_management_alert_assigned_monthly`
Missing description
diff --git a/doc/user/admin_area/merge_requests_approvals.md b/doc/user/admin_area/merge_requests_approvals.md
index d6ffde7be95..e8c435a2b5e 100644
--- a/doc/user/admin_area/merge_requests_approvals.md
+++ b/doc/user/admin_area/merge_requests_approvals.md
@@ -31,3 +31,5 @@ maintainers from allowing users to approve merge requests if they have submitted
any commits to the source branch.
- **Prevent users from modifying merge request approvers list**. Prevents users from
modifying the approvers list in project settings or in individual merge requests.
+
+Also read the [project level merge request approval rules](../project/merge_requests/merge_request_approvals.md), which are affected by instance level rules.
diff --git a/lib/api/entities/user_public.rb b/lib/api/entities/user_public.rb
index 15e9b905bef..685adb1dd10 100644
--- a/lib/api/entities/user_public.rb
+++ b/lib/api/entities/user_public.rb
@@ -14,6 +14,7 @@ module API
expose :two_factor_enabled?, as: :two_factor_enabled
expose :external
expose :private_profile
+ expose :commit_email
end
end
end
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index c915f0ee35b..2247984b86d 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -39,7 +39,7 @@ module Banzai
end
end
- doc.css('pre.code.math').each do |el|
+ doc.css('pre.code.language-math').each do |el|
el[STYLE_ATTRIBUTE] = 'display'
el[:class] += " #{TAG_CLASS}"
end
diff --git a/lib/banzai/filter/suggestion_filter.rb b/lib/banzai/filter/suggestion_filter.rb
index ae093580001..56a14ec0737 100644
--- a/lib/banzai/filter/suggestion_filter.rb
+++ b/lib/banzai/filter/suggestion_filter.rb
@@ -10,7 +10,7 @@ module Banzai
def call
return doc unless suggestions_filter_enabled?
- doc.search('pre.suggestion > code').each do |node|
+ doc.search('pre.language-suggestion > code').each do |node|
node.add_class(TAG_CLASS)
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 1d3bbe43344..731a2bb4c77 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -37,7 +37,7 @@ module Banzai
begin
code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, node.text), tag: language)
- css_classes << " #{language}" if language
+ css_classes << " language-#{language}" if language
rescue
# Gracefully handle syntax highlighter bugs/errors to ensure users can
# still access an issue/comment/etc. First, retry with the plain text
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index d3f030c3b36..3f06ab261c2 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -17,12 +17,14 @@ module Gitlab
Config::Yaml::Tags::TagError
].freeze
- attr_reader :root
+ attr_reader :root, :context, :ref
- def initialize(config, project: nil, sha: nil, user: nil, parent_pipeline: nil)
+ def initialize(config, project: nil, sha: nil, user: nil, parent_pipeline: nil, ref: nil)
@context = build_context(project: project, sha: sha, user: user, parent_pipeline: parent_pipeline)
@context.set_deadline(TIMEOUT_SECONDS)
+ @ref = ref
+
@config = expand_config(config)
@root = Entry::Root.new(@config)
diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb
index c3fbd0c9e24..8f1c49563f2 100644
--- a/lib/gitlab/ci/pipeline/chain/config/process.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/process.rb
@@ -14,6 +14,7 @@ module Gitlab
result = ::Gitlab::Ci::YamlProcessor.new(
@command.config_content, {
project: project,
+ ref: @pipeline.ref,
sha: @pipeline.sha,
user: current_user,
parent_pipeline: parent_pipeline
diff --git a/lib/gitlab/diff/suggestions_parser.rb b/lib/gitlab/diff/suggestions_parser.rb
index 6e17ffaf6ff..dc65c7ccf9c 100644
--- a/lib/gitlab/diff/suggestions_parser.rb
+++ b/lib/gitlab/diff/suggestions_parser.rb
@@ -17,7 +17,7 @@ module Gitlab
no_original_data: true,
suggestions_filter_enabled: supports_suggestion)
doc = Nokogiri::HTML(html)
- suggestion_nodes = doc.search('pre.suggestion')
+ suggestion_nodes = doc.search('pre.language-suggestion')
return [] if suggestion_nodes.empty?
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 55ff3c6caf1..75d6b949874 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -102,12 +102,6 @@ module Gitlab
end
end
- def file(name, version)
- wrapped_gitaly_errors do
- gitaly_find_file(name, version)
- end
- end
-
# options:
# :page - The Integer page number.
# :per_page - The number of items per page.
@@ -161,13 +155,6 @@ module Gitlab
nil
end
- def gitaly_find_file(name, version)
- wiki_file = gitaly_wiki_client.find_file(name, version)
- return unless wiki_file
-
- Gitlab::Git::WikiFile.new(wiki_file)
- end
-
def gitaly_list_pages(limit: 0, sort: nil, direction_desc: false, load_content: false)
params = { limit: limit, sort: sort, direction_desc: direction_desc }
diff --git a/lib/gitlab/git/wiki_file.rb b/lib/gitlab/git/wiki_file.rb
index 7f09173f05c..c56a17c52f3 100644
--- a/lib/gitlab/git/wiki_file.rb
+++ b/lib/gitlab/git/wiki_file.rb
@@ -5,25 +5,11 @@ module Gitlab
class WikiFile
attr_reader :mime_type, :raw_data, :name, :path
- # This class wraps Gitlab::GitalyClient::WikiFile
- def initialize(gitaly_file)
- @mime_type = gitaly_file.mime_type
- @raw_data = gitaly_file.raw_data
- @name = gitaly_file.name
- @path = gitaly_file.path
- end
-
- def self.from_blob(blob)
- hash = {
- name: File.basename(blob.name),
- mime_type: blob.mime_type,
- path: blob.path,
- raw_data: blob.data
- }
-
- gitaly_file = Gitlab::GitalyClient::WikiFile.new(hash)
-
- Gitlab::Git::WikiFile.new(gitaly_file)
+ def initialize(blob)
+ @mime_type = blob.mime_type
+ @raw_data = blob.data
+ @name = File.basename(blob.name)
+ @path = blob.path
end
end
end
diff --git a/lib/gitlab/gitaly_client/attributes_bag.rb b/lib/gitlab/gitaly_client/attributes_bag.rb
index f935281ac2e..74e6279708e 100644
--- a/lib/gitlab/gitaly_client/attributes_bag.rb
+++ b/lib/gitlab/gitaly_client/attributes_bag.rb
@@ -3,7 +3,7 @@
module Gitlab
module GitalyClient
# This module expects an `ATTRS` const to be defined on the subclass
- # See GitalyClient::WikiFile for an example
+ # See GitalyClient::WikiPage for an example
module AttributesBag
extend ActiveSupport::Concern
diff --git a/lib/gitlab/gitaly_client/wiki_file.rb b/lib/gitlab/gitaly_client/wiki_file.rb
deleted file mode 100644
index ef2b23732d1..00000000000
--- a/lib/gitlab/gitaly_client/wiki_file.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module GitalyClient
- class WikiFile
- ATTRS = %i(name mime_type path raw_data).freeze
-
- include AttributesBag
- end
- end
-end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 9034edb6263..fecc2b7023d 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -153,32 +153,6 @@ module Gitlab
versions
end
- def find_file(name, revision)
- request = Gitaly::WikiFindFileRequest.new(
- repository: @gitaly_repo,
- name: encode_binary(name),
- revision: encode_binary(revision)
- )
-
- response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_find_file, request, timeout: GitalyClient.fast_timeout)
- wiki_file = nil
-
- response.each do |message|
- next unless message.name.present? || wiki_file
-
- if wiki_file
- wiki_file.raw_data = "#{wiki_file.raw_data}#{message.raw_data}"
- else
- wiki_file = GitalyClient::WikiFile.new(message.to_h)
- # All gRPC strings in a response are frozen, so we get
- # an unfrozen version here so appending in the else clause below doesn't blow up.
- wiki_file.raw_data = wiki_file.raw_data.dup
- end
- end
-
- wiki_file
- end
-
private
# If a block is given and the yielded value is truthy, iteration will be
diff --git a/lib/gitlab/graphql/negatable_arguments.rb b/lib/gitlab/graphql/negatable_arguments.rb
new file mode 100644
index 00000000000..b4ab31ed51a
--- /dev/null
+++ b/lib/gitlab/graphql/negatable_arguments.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module NegatableArguments
+ class TypeDefiner
+ def initialize(resolver_class, type_definition)
+ @resolver_class = resolver_class
+ @type_definition = type_definition
+ end
+
+ def define!
+ negated_params_type.instance_eval(&@type_definition)
+ end
+
+ def negated_params_type
+ @negated_params_type ||= existing_type || build_type
+ end
+
+ private
+
+ def existing_type
+ ::Types.const_get(type_class_name, false) if ::Types.const_defined?(type_class_name)
+ end
+
+ def build_type
+ klass = Class.new(::Types::BaseInputObject)
+ ::Types.const_set(type_class_name, klass)
+ klass
+ end
+
+ def type_class_name
+ @type_class_name ||= begin
+ base_name = @resolver_class.name.sub('Resolvers::', '')
+ base_name + 'NegatedParamsType'
+ end
+ end
+ end
+
+ def negated(param_key: :not, &block)
+ definer = ::Gitlab::Graphql::NegatableArguments::TypeDefiner.new(self, block)
+ definer.define!
+
+ argument param_key, definer.negated_params_type,
+ required: false,
+ description: <<~MD
+ List of negated arguments.
+ Warning: this argument is experimental and a subject to change in future.
+ MD
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
index def7b58a852..f73ac628bce 100644
--- a/lib/gitlab/repository_set_cache.rb
+++ b/lib/gitlab/repository_set_cache.rb
@@ -49,5 +49,20 @@ module Gitlab
write(key, yield)
end
+
+ # Searches the cache set using SSCAN with the MATCH option. The MATCH
+ # parameter is the pattern argument.
+ # See https://redis.io/commands/scan#the-match-option for more information.
+ # Returns an Enumerator that enumerates all SSCAN hits.
+ def search(key, pattern, &block)
+ full_key = cache_key(key)
+
+ with do |redis|
+ exists = redis.exists(full_key)
+ write(key, yield) unless exists
+
+ redis.sscan_each(full_key, match: pattern)
+ end
+ end
end
end
diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml
index 40a03cc2127..db44b3a4386 100644
--- a/lib/gitlab/usage_data_counters/known_events/common.yml
+++ b/lib/gitlab/usage_data_counters/known_events/common.yml
@@ -247,6 +247,12 @@
category: incident_management_alerts
aggregation: weekly
feature_flag: usage_data_incident_management_alert_create_incident
+# Incident management on-call
+- name: i_incident_management_oncall_notification_sent
+ redis_slot: incident_management
+ category: incident_management_oncall
+ aggregation: weekly
+ feature_flag: usage_data_i_incident_management_oncall_notification_sent
# Testing category
- name: i_testing_test_case_parsed
category: testing
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e4e877f5f43..8feaf10d5d8 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -817,9 +817,6 @@ msgstr ""
msgid "%{spanStart}in%{spanEnd} %{errorFn}"
msgstr ""
-msgid "%{startDate} - %{endDate}"
-msgstr ""
-
msgid "%{start} to %{end}"
msgstr ""
@@ -5649,6 +5646,12 @@ msgstr ""
msgid "Capacity threshold"
msgstr ""
+msgid "CascadingSettings|cannot be changed because it is locked by an ancestor"
+msgstr ""
+
+msgid "CascadingSettings|cannot be nil when locking the attribute"
+msgstr ""
+
msgid "Certain user content will be moved to a system-wide \"Ghost User\" in order to maintain content for posterity. For further information, please refer to the %{link_start}user account deletion documentation.%{link_end}"
msgstr ""
@@ -8573,7 +8576,7 @@ msgstr ""
msgid "ContributionAnalytics|Last week"
msgstr ""
-msgid "ContributionAnalytics|Merge Requests"
+msgid "ContributionAnalytics|Merge requests"
msgstr ""
msgid "ContributionAnalytics|No issues for the selected time period."
@@ -9609,6 +9612,24 @@ msgstr ""
msgid "DNS"
msgstr ""
+msgid "DORA4Metrics|%{startDate} - %{endDate}"
+msgstr ""
+
+msgid "DORA4Metrics|Date"
+msgstr ""
+
+msgid "DORA4Metrics|Deployments"
+msgstr ""
+
+msgid "DORA4Metrics|Deployments charts"
+msgstr ""
+
+msgid "DORA4Metrics|Something went wrong while getting deployment frequency data"
+msgstr ""
+
+msgid "DORA4Metrics|These charts display the frequency of deployments to the production environment, as part of the DORA 4 metrics. The environment must be named %{codeStart}production%{codeEnd} for its data to appear in these charts."
+msgstr ""
+
msgid "Dashboard"
msgstr ""
@@ -10595,24 +10616,6 @@ msgstr ""
msgid "Deployment Frequency"
msgstr ""
-msgid "DeploymentFrequencyCharts|%{startDate} - %{endDate}"
-msgstr ""
-
-msgid "DeploymentFrequencyCharts|Date"
-msgstr ""
-
-msgid "DeploymentFrequencyCharts|Deployments"
-msgstr ""
-
-msgid "DeploymentFrequencyCharts|Deployments charts"
-msgstr ""
-
-msgid "DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data"
-msgstr ""
-
-msgid "DeploymentFrequencyCharts|These charts display the frequency of deployments to the production environment, as part of the DORA 4 metrics. The environment must be named %{codeStart}production%{codeEnd} for its data to appear in these charts."
-msgstr ""
-
msgid "Deployments"
msgstr ""
@@ -11115,9 +11118,6 @@ msgstr[1] ""
msgid "Dismiss DevOps Report introduction"
msgstr ""
-msgid "Dismiss Merge Request promotion"
-msgstr ""
-
msgid "Dismiss Value Stream Analytics introduction box"
msgstr ""
@@ -19440,6 +19440,9 @@ msgstr ""
msgid "Merge request (MR) approvals"
msgstr ""
+msgid "Merge request analytics"
+msgstr ""
+
msgid "Merge request approval settings have been updated."
msgstr ""
@@ -26758,16 +26761,19 @@ msgstr ""
msgid "Runners|Can run untagged jobs"
msgstr ""
+msgid "Runners|Command to register runner"
+msgstr ""
+
msgid "Runners|Copy instructions"
msgstr ""
msgid "Runners|Description"
msgstr ""
-msgid "Runners|Download Latest Binary"
+msgid "Runners|Download and install binary"
msgstr ""
-msgid "Runners|Download and Install Binary"
+msgid "Runners|Download latest binary"
msgstr ""
msgid "Runners|Group"
@@ -26776,7 +26782,7 @@ msgstr ""
msgid "Runners|IP Address"
msgstr ""
-msgid "Runners|Install a Runner"
+msgid "Runners|Install a runner"
msgstr ""
msgid "Runners|Last contact"
@@ -26800,9 +26806,6 @@ msgstr ""
msgid "Runners|Protected"
msgstr ""
-msgid "Runners|Register Runner"
-msgstr ""
-
msgid "Runners|Revision"
msgstr ""
@@ -36756,9 +36759,6 @@ msgstr ""
msgid "mrWidget|Jump to first unresolved thread"
msgstr ""
-msgid "mrWidget|Learn more about resolving conflicts"
-msgstr ""
-
msgid "mrWidget|Loading deployment statistics"
msgstr ""
@@ -36873,9 +36873,6 @@ msgstr ""
msgid "mrWidget|This action will start a merge train when pipeline %{pipelineLink} succeeds."
msgstr ""
-msgid "mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected."
-msgstr ""
-
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr ""
diff --git a/package.json b/package.json
index 795fb91340a..e909612934f 100644
--- a/package.json
+++ b/package.json
@@ -150,7 +150,7 @@
"vue-loader": "^15.9.6",
"vue-router": "3.4.9",
"vue-template-compiler": "^2.6.12",
- "vue-virtual-scroll-list": "^1.4.4",
+ "vue-virtual-scroll-list": "^1.4.7",
"vuedraggable": "^2.23.0",
"vuex": "^3.6.0",
"web-vitals": "^0.2.4",
diff --git a/spec/benchmarks/banzai_benchmark.rb b/spec/benchmarks/banzai_benchmark.rb
index 4cf079b2130..05c41eed889 100644
--- a/spec/benchmarks/banzai_benchmark.rb
+++ b/spec/benchmarks/banzai_benchmark.rb
@@ -54,9 +54,10 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
context 'pipelines' do
it 'benchmarks several pipelines' do
- path = 'images/example.jpg'
- gitaly_wiki_file = Gitlab::GitalyClient::WikiFile.new(path: path)
- allow(wiki).to receive(:find_file).with(path, load_content: false).and_return(Gitlab::Git::WikiFile.new(gitaly_wiki_file))
+ name = 'example.jpg'
+ path = "images/#{name}"
+ blob = double(name: name, path: path, mime_type: 'image/jpeg', data: nil)
+ allow(wiki).to receive(:find_file).with(path, load_content: false).and_return(Gitlab::Git::WikiFile.new(blob))
allow(wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
puts "\n--> Benchmarking Full, Wiki, and Plain pipelines\n"
diff --git a/spec/controllers/registrations/welcome_controller_spec.rb b/spec/controllers/registrations/welcome_controller_spec.rb
index d32c936b8c9..008259a8bfa 100644
--- a/spec/controllers/registrations/welcome_controller_spec.rb
+++ b/spec/controllers/registrations/welcome_controller_spec.rb
@@ -60,8 +60,10 @@ RSpec.describe Registrations::WelcomeController do
end
describe '#update' do
+ let(:email_opted_in) { '0' }
+
subject(:update) do
- patch :update, params: { user: { role: 'software_developer', setup_for_company: 'false' } }
+ patch :update, params: { user: { role: 'software_developer', setup_for_company: 'false', email_opted_in: email_opted_in } }
end
context 'without a signed in user' do
@@ -74,6 +76,24 @@ RSpec.describe Registrations::WelcomeController do
end
it { is_expected.to redirect_to(dashboard_projects_path)}
+
+ context 'when the user opted in' do
+ let(:email_opted_in) { '1' }
+
+ it 'sets the email_opted_in field' do
+ subject
+
+ expect(controller.current_user.email_opted_in).to eq(true)
+ end
+ end
+
+ context 'when the user opted out' do
+ it 'sets the email_opted_in field' do
+ subject
+
+ expect(controller.current_user.email_opted_in).to eq(false)
+ end
+ end
end
end
end
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index e84b300a748..3208ad82c03 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -63,8 +63,8 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
end
aggregate_failures 'parses fenced code blocks' do
- expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.c')
- expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.python')
+ expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.language-c')
+ expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.language-python')
end
aggregate_failures 'parses mermaid code block' do
@@ -288,9 +288,10 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
@wiki = @feat.wiki
@wiki_page = @feat.wiki_page
- path = 'images/example.jpg'
- gitaly_wiki_file = Gitlab::GitalyClient::WikiFile.new(path: path)
- expect(@wiki).to receive(:find_file).with(path, load_content: false).and_return(Gitlab::Git::WikiFile.new(gitaly_wiki_file))
+ name = 'example.jpg'
+ path = "images/#{name}"
+ blob = double(name: name, path: path, mime_type: 'image/jpeg', data: nil)
+ expect(@wiki).to receive(:find_file).with(path, load_content: false).and_return(Gitlab::Git::WikiFile.new(blob))
allow(@wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
@html = markdown(@feat.raw_markdown, { pipeline: :wiki, wiki: @wiki, page_slug: @wiki_page.slug })
diff --git a/spec/features/registrations/welcome_spec.rb b/spec/features/registrations/welcome_spec.rb
new file mode 100644
index 00000000000..74320b69f19
--- /dev/null
+++ b/spec/features/registrations/welcome_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Welcome screen' do
+ let(:user) { create(:user) }
+
+ before do
+ gitlab_sign_in(user)
+
+ visit users_sign_up_welcome_path
+ end
+
+ it 'shows the email opt in' do
+ select 'Software Developer', from: 'user_role'
+ check 'user_email_opted_in'
+ click_button 'Get started!'
+
+ expect(user.reload.email_opted_in).to eq(true)
+ end
+end
diff --git a/spec/finders/repositories/branch_names_finder_spec.rb b/spec/finders/repositories/branch_names_finder_spec.rb
new file mode 100644
index 00000000000..4d8bfcc0f20
--- /dev/null
+++ b/spec/finders/repositories/branch_names_finder_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Repositories::BranchNamesFinder do
+ let(:project) { create(:project, :repository) }
+
+ let(:branch_names_finder) { described_class.new(project.repository, search: 'conflict-*') }
+
+ describe '#execute' do
+ subject(:execute) { branch_names_finder.execute }
+
+ it 'filters branch names' do
+ expect(execute).to contain_exactly(
+ 'conflict-binary-file',
+ 'conflict-resolvable',
+ 'conflict-contains-conflict-markers',
+ 'conflict-missing-side',
+ 'conflict-start',
+ 'conflict-non-utf8',
+ 'conflict-too-large'
+ )
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/public.json b/spec/fixtures/api/schemas/public_api/v4/user/public.json
index faa126b65f2..ee848eda9ed 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/public.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/public.json
@@ -70,6 +70,7 @@
"can_create_group": { "type": "boolean" },
"can_create_project": { "type": "boolean" },
"two_factor_enabled": { "type": "boolean" },
- "external": { "type": "boolean" }
+ "external": { "type": "boolean" },
+ "commit_email": { "type": "string" }
}
}
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
index 339aac9f349..a79917bfd48 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -98,6 +98,42 @@ export const pipelineData = {
],
};
+export const invalidNeedsData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test' }],
+ },
+ {
+ name: 'test_2',
+ jobs: [{ script: 'yarn karma', stage: 'test' }],
+ },
+ ],
+ },
+ {
+ name: 'deploy',
+ groups: [
+ {
+ name: 'deploy_1',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['invalid_job'] }],
+ },
+ ],
+ },
+ ],
+};
+
export const parallelNeedData = {
stages: [
{
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index aeb567a8869..6deec06e344 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,11 +1,13 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
+import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
+import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
-import { pipelineData, singleStageData } from './mock_data';
+import { invalidNeedsData, pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
const defaultProps = { pipelineData };
@@ -16,19 +18,28 @@ describe('pipeline graph component', () => {
propsData: {
...props,
},
+ stubs: { LinksLayer, LinksInner },
+ data() {
+ return {
+ measurements: {
+ width: 1000,
+ height: 1000,
+ },
+ };
+ },
});
};
- const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
- const findAlert = () => wrapper.find(GlAlert);
- const findAllStagePills = () => wrapper.findAll(StagePill);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAllJobPills = () => wrapper.findAll(JobPill);
const findAllStageBackgroundElements = () => wrapper.findAll('[data-testid="stage-background"]');
+ const findAllStagePills = () => wrapper.findAllComponents(StagePill);
+ const findLinksLayer = () => wrapper.findComponent(LinksLayer);
+ const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
const findStageBackgroundElementAt = (index) => findAllStageBackgroundElements().at(index);
- const findAllJobPills = () => wrapper.findAll(JobPill);
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('with no data', () => {
@@ -36,7 +47,7 @@ describe('pipeline graph component', () => {
wrapper = createComponent({ pipelineData: {} });
});
- it('renders an empty section', () => {
+ it('does not render the graph', () => {
expect(wrapper.text()).toBe(wrapper.vm.$options.errorTexts[EMPTY_PIPELINE_DATA]);
expect(findPipelineGraph().exists()).toBe(false);
expect(findAllStagePills()).toHaveLength(0);
@@ -74,10 +85,11 @@ describe('pipeline graph component', () => {
describe('with error while rendering the links with needs', () => {
beforeEach(() => {
- wrapper = createComponent();
+ wrapper = createComponent({ pipelineData: invalidNeedsData });
});
it('renders the error that link could not be drawn', () => {
+ expect(findLinksLayer().exists()).toBe(true);
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[DRAW_FAILURE]);
});
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
index fc51825f15b..c37f6415898 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
@@ -21,7 +21,11 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
option="[object Object]"
thresholds=""
width="0"
- />
+ >
+ <template />
+
+ <template />
+ </glareachart-stub>
</div>
</div>
`;
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index dc2f227b29c..fee78d3af94 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,4 +1,3 @@
-import { GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { removeBreakLine } from 'helpers/text_helper';
@@ -10,7 +9,6 @@ describe('MRWidgetConflicts', () => {
let mergeRequestWidgetGraphql = null;
const path = '/conflicts';
- const findPopover = () => wrapper.find(GlPopover);
const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button');
const findMergeLocalButton = () => wrapper.findByTestId('merge-locally-button');
@@ -219,12 +217,8 @@ describe('MRWidgetConflicts', () => {
});
});
- it('sets resolve button as disabled', () => {
- expect(findResolveButton().attributes('disabled')).toBe('true');
- });
-
- it('shows the popover', () => {
- expect(findPopover().exists()).toBe(true);
+ it('should not allow you to resolve the conflicts', () => {
+ expect(findResolveButton().exists()).toBe(false);
});
});
@@ -241,12 +235,9 @@ describe('MRWidgetConflicts', () => {
});
});
- it('sets resolve button as disabled', () => {
- expect(findResolveButton().attributes('disabled')).toBe(undefined);
- });
-
- it('does not show the popover', () => {
- expect(findPopover().exists()).toBe(false);
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().text()).toContain('Resolve conflicts');
+ expect(findResolveButton().attributes('href')).toEqual(TEST_HOST);
});
});
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
index 01f7f3d49c7..bc1545014d7 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
@@ -98,9 +98,21 @@ export const mockGraphqlInstructions = {
data: {
runnerSetup: {
installInstructions:
- "# Download the binary for your system\nsudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64\n\n# Give it permissions to execute\nsudo chmod +x /usr/local/bin/gitlab-runner\n\n# Create a GitLab CI user\nsudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash\n\n# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start\n",
+ '# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start',
registerInstructions:
- 'sudo gitlab-runner register --url http://192.168.1.81:3000/ --registration-token GE5gsjeep_HAtBf9s3Yz',
+ 'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token $REGISTRATION_TOKEN',
+ __typename: 'RunnerSetup',
+ },
+ },
+};
+
+export const mockGraphqlInstructionsWindows = {
+ data: {
+ runnerSetup: {
+ installInstructions:
+ '# Windows runner, then run\n.gitlab-runner.exe install\n.gitlab-runner.exe start',
+ registerInstructions:
+ './gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token $REGISTRATION_TOKEN',
__typename: 'RunnerSetup',
},
},
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
new file mode 100644
index 00000000000..4033c943b82
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -0,0 +1,184 @@
+import { GlAlert, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+
+import {
+ mockGraphqlRunnerPlatforms,
+ mockGraphqlInstructions,
+ mockGraphqlInstructionsWindows,
+} from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('RunnerInstructionsModal component', () => {
+ let wrapper;
+ let fakeApollo;
+ let runnerPlatformsHandler;
+ let runnerSetupInstructionsHandler;
+
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findPlatformButtons = () => wrapper.findAllByTestId('platform-button');
+ const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
+ const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
+ const findRegisterCommand = () => wrapper.findByTestId('register-command');
+
+ const createComponent = () => {
+ const requestHandlers = [
+ [getRunnerPlatformsQuery, runnerPlatformsHandler],
+ [getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ wrapper = extendedWrapper(
+ shallowMount(RunnerInstructionsModal, {
+ propsData: {
+ modalId: 'runner-instructions-modal',
+ },
+ localVue,
+ apolloProvider: fakeApollo,
+ }),
+ );
+ };
+
+ beforeEach(async () => {
+ runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
+ runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
+
+ createComponent();
+
+ await nextTick();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should not show alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should contain a number of platforms buttons', () => {
+ expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
+
+ const buttons = findPlatformButtons();
+
+ expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
+ });
+
+ it('should contain a number of dropdown items for the architecture options', () => {
+ expect(findArchitectureDropdownItems()).toHaveLength(
+ mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
+ );
+ });
+
+ describe('should display default instructions', () => {
+ const { installInstructions, registerInstructions } = mockGraphqlInstructions.data.runnerSetup;
+
+ it('runner instructions are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
+ platform: 'linux',
+ architecture: 'amd64',
+ });
+ });
+
+ it('binary instructions are shown', () => {
+ const instructions = findBinaryInstructions().text();
+
+ expect(instructions).toBe(installInstructions);
+ });
+
+ it('register command is shown', () => {
+ const instructions = findRegisterCommand().text();
+
+ expect(instructions).toBe(registerInstructions);
+ });
+ });
+
+ describe('after a platform and architecture are selected', () => {
+ const {
+ installInstructions,
+ registerInstructions,
+ } = mockGraphqlInstructionsWindows.data.runnerSetup;
+
+ beforeEach(async () => {
+ runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
+
+ findPlatformButtons().at(2).vm.$emit('click'); // another option, happens to be windows
+ await nextTick();
+
+ findArchitectureDropdownItems().at(1).vm.$emit('click'); // another option
+ await nextTick();
+ });
+
+ it('runner instructions are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
+ platform: 'windows',
+ architecture: '386',
+ });
+ });
+
+ it('other binary instructions are shown', () => {
+ const instructions = findBinaryInstructions().text();
+
+ expect(instructions).toBe(installInstructions);
+ });
+
+ it('register command is shown', () => {
+ const command = findRegisterCommand().text();
+
+ expect(command).toBe(registerInstructions);
+ });
+ });
+
+ describe('when apollo is loading', () => {
+ it('should show a skeleton loader', async () => {
+ createComponent();
+ expect(findSkeletonLoader().exists()).toBe(true);
+ expect(findGlLoadingIcon().exists()).toBe(false);
+
+ await nextTick(); // wait for platforms
+
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('once loaded, should not show a loading state', async () => {
+ createComponent();
+
+ await nextTick(); // wait for platforms
+ await nextTick(); // wait for architectures
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when instructions cannot be loaded', () => {
+ beforeEach(async () => {
+ runnerSetupInstructionsHandler.mockRejectedValue();
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should show alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('should not show instructions', () => {
+ expect(findBinaryInstructions().exists()).toBe(false);
+ expect(findRegisterCommand().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
index 4446c3cd88f..23f8d6afcb5 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -1,147 +1,41 @@
-import { GlAlert } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import getRunnerPlatforms from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
-import getRunnerSetupInstructions from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
-
-import { mockGraphqlRunnerPlatforms, mockGraphqlInstructions } from './mock_data';
-
-const projectPath = 'gitlab-org/gitlab';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
describe('RunnerInstructions component', () => {
let wrapper;
- let fakeApollo;
- let runnerPlatformsHandler;
- let runnerSetupInstructionsHandler;
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findModalButton = () => wrapper.find('[data-testid="show-modal-button"]');
- const findPlatformButtons = () => wrapper.findAll('[data-testid="platform-button"]');
- const findArchitectureDropdownItems = () =>
- wrapper.findAll('[data-testid="architecture-dropdown-item"]');
- const findBinaryInstructionsSection = () => wrapper.find('[data-testid="binary-instructions"]');
- const findRunnerInstructionsSection = () => wrapper.find('[data-testid="runner-instructions"]');
+ const findModalButton = () => wrapper.findByTestId('show-modal-button');
+ const findModal = () => wrapper.findComponent(RunnerInstructionsModal);
const createComponent = () => {
- const requestHandlers = [
- [getRunnerPlatforms, runnerPlatformsHandler],
- [getRunnerSetupInstructions, runnerSetupInstructionsHandler],
- ];
-
- fakeApollo = createMockApollo(requestHandlers);
-
- wrapper = shallowMount(RunnerInstructions, {
- provide: {
- projectPath,
- },
- localVue,
- apolloProvider: fakeApollo,
- });
+ wrapper = extendedWrapper(shallowMount(RunnerInstructions));
};
- beforeEach(async () => {
- runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
- runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
-
+ beforeEach(() => {
createComponent();
-
- await wrapper.vm.$nextTick();
});
afterEach(() => {
wrapper.destroy();
- wrapper = null;
- });
-
- it('should not show alert', () => {
- expect(findAlert().exists()).toBe(false);
});
it('should show the "Show Runner installation instructions" button', () => {
- const button = findModalButton();
-
- expect(button.exists()).toBe(true);
- expect(button.text()).toBe('Show Runner installation instructions');
- });
-
- it('should contain a number of platforms buttons', () => {
- const buttons = findPlatformButtons();
-
- expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
- });
-
- it('should contain a number of dropdown items for the architecture options', () => {
- const platformButton = findPlatformButtons().at(0);
- platformButton.vm.$emit('click');
-
- return wrapper.vm.$nextTick(() => {
- const dropdownItems = findArchitectureDropdownItems();
-
- expect(dropdownItems).toHaveLength(
- mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
- );
- });
+ expect(findModalButton().exists()).toBe(true);
+ expect(findModalButton().text()).toBe('Show Runner installation instructions');
});
- it('should display the binary installation instructions for a selected architecture', async () => {
- const platformButton = findPlatformButtons().at(0);
- platformButton.vm.$emit('click');
-
- await wrapper.vm.$nextTick();
-
- const dropdownItem = findArchitectureDropdownItems().at(0);
- dropdownItem.vm.$emit('click');
-
- await wrapper.vm.$nextTick();
-
- const runner = findBinaryInstructionsSection();
-
- expect(runner.text()).toMatch('sudo chmod +x /usr/local/bin/gitlab-runner');
- expect(runner.text()).toMatch(
- `sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash`,
- );
- expect(runner.text()).toMatch(
- 'sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner',
- );
- expect(runner.text()).toMatch('sudo gitlab-runner start');
+ it('should not render the modal once mounted', () => {
+ expect(findModal().exists()).toBe(false);
});
- it('should display the runner register instructions for a selected architecture', async () => {
- const platformButton = findPlatformButtons().at(0);
- platformButton.vm.$emit('click');
-
- await wrapper.vm.$nextTick();
-
- const dropdownItem = findArchitectureDropdownItems().at(0);
- dropdownItem.vm.$emit('click');
-
- await wrapper.vm.$nextTick();
-
- const runner = findRunnerInstructionsSection();
-
- expect(runner.text()).toMatch(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
- });
-
- describe('when instructions cannot be loaded', () => {
- beforeEach(async () => {
- runnerSetupInstructionsHandler.mockRejectedValue();
-
- createComponent();
-
- await wrapper.vm.$nextTick();
- });
+ it('should render the modal once clicked', async () => {
+ findModalButton().vm.$emit('click');
- it('should show alert', () => {
- expect(findAlert().exists()).toBe(true);
- });
+ await nextTick();
- it('should not show instructions', () => {
- expect(findBinaryInstructionsSection().exists()).toBe(false);
- expect(findRunnerInstructionsSection().exists()).toBe(false);
- });
+ expect(findModal().exists()).toBe(true);
});
});
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 09f5181107d..aec6c6c6708 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -189,6 +189,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
+ context 'with negated label argument' do
+ let_it_be(:label) { merge_request_6.labels.first }
+ let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) }
+
+ it 'excludes merge requests with given label from selection' do
+ result = resolve_mr(project, not: { labels: [label.title] })
+
+ expect(result).not_to include(merge_request_6, with_label)
+ end
+ end
+
context 'with merged_after and merged_before arguments' do
before do
merge_request_1.metrics.update!(merged_at: 10.days.ago)
@@ -221,6 +232,14 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
+ context 'with negated milestone argument' do
+ it 'filters out merge requests with given milestone title' do
+ result = resolve_mr(project, not: { milestone_title: milestone.title })
+
+ expect(result).not_to include(merge_request_with_milestone)
+ end
+ end
+
describe 'combinations' do
it 'requires all filters' do
create(:merge_request, :closed, **common_attrs, source_branch: merge_request_4.source_branch)
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 33f4953b07b..7a8c6464acc 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -243,6 +243,7 @@ RSpec.describe GitlabSchema.types['Project'] do
:assignee_username,
:reviewer_username,
:milestone_title,
+ :not,
:sort
)
end
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 7fcd5ae880a..120dbe7cb49 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -121,27 +121,13 @@ RSpec.describe AvatarsHelper do
end
end
- context "when :avatar_cache_for_email flag is enabled" do
- before do
- stub_feature_flags(avatar_cache_for_email: true)
- end
-
- it_behaves_like "returns avatar for email"
+ it_behaves_like "returns avatar for email"
- it "caches the request" do
- expect(User).to receive(:find_by_any_email).once.and_call_original
-
- expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
- expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
- end
- end
-
- context "when :avatar_cache_for_email flag is disabled" do
- before do
- stub_feature_flags(avatar_cache_for_email: false)
- end
+ it "caches the request" do
+ expect(User).to receive(:find_by_any_email).once.and_call_original
- it_behaves_like "returns avatar for email"
+ expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
+ expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 91e9075f575..08a20e87f4b 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -575,7 +575,7 @@ FooBar
it 'preserves code color scheme' do
object = create_object("```ruby\ndef test\n 'hello world'\nend\n```")
- expected = "<pre class=\"code highlight js-syntax-highlight ruby\">" \
+ expected = "<pre class=\"code highlight js-syntax-highlight language-ruby\">" \
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
"</code></pre>"
diff --git a/spec/initializers/active_record_locking_spec.rb b/spec/initializers/active_record_locking_spec.rb
index e979fa0b793..735ef7b916b 100644
--- a/spec/initializers/active_record_locking_spec.rb
+++ b/spec/initializers/active_record_locking_spec.rb
@@ -11,13 +11,13 @@ RSpec.describe 'ActiveRecord locking' do
end
it 'can be updated' do
- issue.update(title: "New title")
+ issue.update!(title: "New title")
expect(issue.reload.lock_version).to eq(new_lock_version)
end
it 'can be deleted' do
- expect { issue.destroy }.to change { Issue.count }.by(-1)
+ expect { issue.destroy! }.to change { Issue.count }.by(-1)
end
end
diff --git a/spec/initializers/fog_google_https_private_urls_spec.rb b/spec/initializers/fog_google_https_private_urls_spec.rb
index 4825525a3d8..f7b21bf850e 100644
--- a/spec/initializers/fog_google_https_private_urls_spec.rb
+++ b/spec/initializers/fog_google_https_private_urls_spec.rb
@@ -13,11 +13,13 @@ RSpec.describe 'Fog::Storage::GoogleXML::File', :fog_requests do
end
let(:file) do
+ # rubocop:disable Rails/SaveBang
directory = storage.directories.create(key: 'data')
directory.files.create(
body: 'Hello World!',
key: 'hello_world.txt'
)
+ # rubocop:enable Rails/SaveBang
end
it 'delegates to #get_https_url' do
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index b0136ce1fef..23626576c0c 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -16,12 +16,8 @@ RSpec.describe Banzai::Filter::GollumTagsFilter do
context 'linking internal images' do
it 'creates img tag if image exists' do
- gollum_file_double = double('Gollum::File',
- mime_type: 'image/jpeg',
- name: 'images/image.jpg',
- path: 'images/image.jpg',
- raw_data: '')
- wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
+ blob = double(mime_type: 'image/jpeg', name: 'images/image.jpg', path: 'images/image.jpg', data: '')
+ wiki_file = Gitlab::Git::WikiFile.new(blob)
expect(wiki).to receive(:find_file).with('images/image.jpg', load_content: false).and_return(wiki_file)
tag = '[[images/image.jpg]]'
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
index 9f6688f4f7d..6d22fa3a001 100644
--- a/spec/lib/banzai/filter/math_filter_spec.rb
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -91,35 +91,35 @@ RSpec.describe Banzai::Filter::MathFilter do
# Display math
it 'adds data-math-style display attribute to display math' do
- doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+ doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
pre = doc.xpath('descendant-or-self::pre').first
expect(pre['data-math-style']).to eq 'display'
end
it 'adds js-render-math class to display math' do
- doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+ doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
pre = doc.xpath('descendant-or-self::pre').first
expect(pre[:class]).to include("js-render-math")
end
it 'ignores code blocks that are not math' do
- input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>'
+ input = '<pre class="code highlight js-syntax-highlight language-plaintext" v-pre="true"><code>2+2</code></pre>'
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'requires the pre to contain both code and math' do
- input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>'
+ input = '<pre class="highlight js-syntax-highlight language-plaintext language-math" v-pre="true"><code>2+2</code></pre>'
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'dollar signs around to display math' do
- doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$')
+ doc = filter('$<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>$')
before = doc.xpath('descendant-or-self::text()[1]').first
after = doc.xpath('descendant-or-self::text()[3]').first
diff --git a/spec/lib/banzai/filter/suggestion_filter_spec.rb b/spec/lib/banzai/filter/suggestion_filter_spec.rb
index 7d6092e21e9..d74bac4898e 100644
--- a/spec/lib/banzai/filter/suggestion_filter_spec.rb
+++ b/spec/lib/banzai/filter/suggestion_filter_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Banzai::Filter::SuggestionFilter do
include FilterSpecHelper
- let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion"><code>foo\n</code></pre>) }
+ let(:input) { %(<pre class="code highlight js-syntax-highlight language-suggestion"><code>foo\n</code></pre>) }
let(:default_context) do
{ suggestions_filter_enabled: true }
end
@@ -26,7 +26,7 @@ RSpec.describe Banzai::Filter::SuggestionFilter do
context 'multi-line suggestions' do
let(:data_attr) { Banzai::Filter::SyntaxHighlightFilter::LANG_PARAMS_ATTR }
- let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) }
+ let(:input) { %(<pre class="code highlight js-syntax-highlight language-suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) }
it 'element has correct data-lang-params' do
doc = filter(input, default_context)
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 78f84ee44f7..16e30604c99 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
end
include_examples "XSS prevention", ""
@@ -38,7 +38,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as that language" do
result = filter('<pre><code lang="ruby">def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
end
include_examples "XSS prevention", "ruby"
@@ -48,7 +48,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext" do
result = filter('<pre><code lang="gnuplot">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
end
include_examples "XSS prevention", "gnuplot"
@@ -63,7 +63,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext but with the correct language attribute and class" do
result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
- expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
end
include_examples "XSS prevention", lang
@@ -75,7 +75,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "includes data-lang-params tag with extra information" do
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>})
- expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
end
include_examples "XSS prevention", lang
@@ -93,7 +93,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "delimits on the first appearance" do
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
- expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
+ expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
end
end
end
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index ab6093e9198..007d310247b 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -297,7 +297,7 @@ RSpec.describe Banzai::Pipeline::WikiPipeline do
mime_type: 'image/jpeg',
name: 'images/image.jpg',
path: 'images/image.jpg',
- raw_data: '')
+ data: '')
wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
markdown = "[[#{wiki_file.path}]]"
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 3eb015a5a22..f3799c58fed 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -83,7 +83,7 @@ module Gitlab
},
'fenced code with inline script' => {
input: '```mypre"><script>alert(3)</script>',
- output: "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n</div>\n</div>"
+ output: "<div>\n<div>\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" v-pre=\"true\"><code><span id=\"LC1\" class=\"line\" lang=\"plaintext\">\"&gt;</span></code></pre>\n</div>\n</div>"
}
}
@@ -353,7 +353,7 @@ module Gitlab
output = <<~HTML
<div>
<div>
- <pre class="code highlight js-syntax-highlight javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
+ <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
</div>
</div>
HTML
@@ -380,7 +380,7 @@ module Gitlab
<div>
<div>class.cpp</div>
<div>
- <pre class="code highlight js-syntax-highlight cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
+ <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include &lt;stdio.h&gt;</span></span>
<span id="LC2" class="line" lang="cpp"></span>
<span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
<span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
diff --git a/spec/lib/gitlab/graphql/negatable_arguments_spec.rb b/spec/lib/gitlab/graphql/negatable_arguments_spec.rb
new file mode 100644
index 00000000000..bc6e25eb018
--- /dev/null
+++ b/spec/lib/gitlab/graphql/negatable_arguments_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Graphql::NegatableArguments do
+ let(:test_resolver) do
+ Class.new(Resolvers::BaseResolver).tap do |klass|
+ klass.extend described_class
+ allow(klass).to receive(:name).and_return('Resolvers::TestResolver')
+ end
+ end
+
+ describe '#negated' do
+ it 'defines :not argument' do
+ test_resolver.negated {}
+
+ expect(test_resolver.arguments['not'].type.name).to eq "Types::TestResolverNegatedParamsType"
+ end
+
+ it 'defines any arguments passed as block' do
+ test_resolver.negated do
+ argument :foo, GraphQL::STRING_TYPE, required: false
+ end
+
+ expect(test_resolver.arguments['not'].type.arguments.keys).to match_array(['foo'])
+ end
+
+ it 'defines all arguments passed as block even if called multiple times' do
+ test_resolver.negated do
+ argument :foo, GraphQL::STRING_TYPE, required: false
+ end
+ test_resolver.negated do
+ argument :bar, GraphQL::STRING_TYPE, required: false
+ end
+
+ expect(test_resolver.arguments['not'].type.arguments.keys).to match_array(%w[foo bar])
+ end
+
+ it 'allows to specify custom argument name' do
+ test_resolver.negated(param_key: :negative) {}
+
+ expect(test_resolver.arguments).to include('negative')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 89917e515d0..48e2a2e9794 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -78,13 +78,8 @@ RSpec.describe Gitlab::Profiler do
end
it 'strips out the private token' do
- expect(custom_logger).to receive(:add) do |severity, _progname, message|
- next if message.include?('spec/')
-
- expect(severity).to eq(Logger::DEBUG)
- expect(message).to include('public').and include(described_class::FILTERED_STRING)
- expect(message).not_to include(private_token)
- end.at_least(1) # This spec could be wrapped in more blocks in the future
+ allow(custom_logger).to receive(:add).and_call_original
+ expect(custom_logger).to receive(:add).with(Logger::DEBUG, anything, 'public [FILTERED]').at_least(1)
custom_logger.debug("public #{private_token}")
end
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index 6bdcc5ea2b7..eaecbb0233d 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -124,6 +124,18 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
end
end
+ describe '#search' do
+ subject do
+ cache.search(:foo, 'val*') do
+ %w[value helloworld notvalmatch]
+ end
+ end
+
+ it 'returns search pattern matches from the key' do
+ is_expected.to contain_exactly('value')
+ end
+ end
+
describe '#include?' do
it 'checks inclusion in the Redis set' do
cache.write(:foo, ['value'])
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index e7681ae5706..a948815171e 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -34,6 +34,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'source_code',
'incident_management',
'incident_management_alerts',
+ 'incident_management_oncall',
'testing',
'issues_edit',
'ci_secrets_management',
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 31066419ee4..d819f28e114 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1362,7 +1362,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories }
let(:ineligible_total_categories) do
- %w[source_code ci_secrets_management incident_management_alerts snippets terraform]
+ %w[source_code ci_secrets_management incident_management_alerts snippets terraform incident_management_oncall]
end
context 'with redis_hll_tracking feature enabled' do
diff --git a/spec/mailers/emails/in_product_marketing_spec.rb b/spec/mailers/emails/in_product_marketing_spec.rb
index e4157eaf5dc..25735e64bdf 100644
--- a/spec/mailers/emails/in_product_marketing_spec.rb
+++ b/spec/mailers/emails/in_product_marketing_spec.rb
@@ -13,6 +13,38 @@ RSpec.describe Emails::InProductMarketing do
describe '#in_product_marketing_email' do
using RSpec::Parameterized::TableSyntax
+ let(:track) { :create }
+ let(:series) { 0 }
+
+ subject { Notify.in_product_marketing_email(user.id, group.id, track, series) }
+
+ include_context 'gitlab email notification'
+
+ it 'sends to the right user with a link to unsubscribe' do
+ aggregate_failures do
+ expect(subject).to deliver_to(user.notification_email)
+ expect(subject).to have_body_text(profile_notifications_url)
+ end
+ end
+
+ context 'when on gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'has custom headers' do
+ aggregate_failures do
+ expect(subject).to deliver_from(described_class::FROM_ADDRESS)
+ expect(subject).to reply_to(described_class::FROM_ADDRESS)
+ expect(subject).to have_header('X-Mailgun-Track', 'yes')
+ expect(subject).to have_header('X-Mailgun-Track-Clicks', 'yes')
+ expect(subject).to have_header('X-Mailgun-Track-Opens', 'yes')
+ expect(subject).to have_header('X-Mailgun-Tag', 'marketing')
+ expect(subject).to have_body_text('%tag_unsubscribe_url%')
+ end
+ end
+ end
+
where(:track, :series) do
:create | 0
:create | 1
@@ -29,8 +61,6 @@ RSpec.describe Emails::InProductMarketing do
end
with_them do
- subject { Notify.in_product_marketing_email(user.id, group.id, track, series) }
-
it 'has the correct subject and content' do
aggregate_failures do
is_expected.to have_subject(subject_line(track, series))
diff --git a/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
new file mode 100644
index 00000000000..ddff9ce32b4
--- /dev/null
+++ b/spec/models/concerns/cascading_namespace_setting_attribute_spec.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe NamespaceSetting, 'CascadingNamespaceSettingAttribute' do
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+
+ def group_settings
+ group.namespace_settings
+ end
+
+ def subgroup_settings
+ subgroup.namespace_settings
+ end
+
+ describe '#delayed_project_removal' do
+ subject(:delayed_project_removal) { subgroup_settings.delayed_project_removal }
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(cascading_namespace_settings: false)
+
+ group_settings.update!(delayed_project_removal: true)
+ end
+
+ it 'does not cascade' do
+ expect(delayed_project_removal).to eq(nil)
+ end
+ end
+
+ context 'when there is no parent' do
+ context 'and the value is not nil' do
+ before do
+ group_settings.update!(delayed_project_removal: true)
+ end
+
+ it 'returns the local value' do
+ expect(group_settings.delayed_project_removal).to eq(true)
+ end
+ end
+
+ context 'and the value is nil' do
+ before do
+ group_settings.update!(delayed_project_removal: nil)
+ stub_application_setting(delayed_project_removal: false)
+ end
+
+ it 'returns the application settings value' do
+ expect(group_settings.delayed_project_removal).to eq(false)
+ end
+ end
+ end
+
+ context 'when parent does not lock the attribute' do
+ context 'and value is not nil' do
+ before do
+ group_settings.update!(delayed_project_removal: false)
+ end
+
+ it 'returns local setting when present' do
+ subgroup_settings.update!(delayed_project_removal: true)
+
+ expect(delayed_project_removal).to eq(true)
+ end
+
+ it 'returns the parent value when local value is nil' do
+ subgroup_settings.update!(delayed_project_removal: nil)
+
+ expect(delayed_project_removal).to eq(false)
+ end
+
+ it 'returns the correct dirty value' do
+ subgroup_settings.delayed_project_removal = true
+
+ expect(delayed_project_removal).to eq(true)
+ end
+
+ it 'does not return the application setting value when parent value is false' do
+ stub_application_setting(delayed_project_removal: true)
+
+ expect(delayed_project_removal).to eq(false)
+ end
+ end
+
+ context 'and the value is nil' do
+ before do
+ group_settings.update!(delayed_project_removal: nil, lock_delayed_project_removal: false)
+ subgroup_settings.update!(delayed_project_removal: nil)
+
+ subgroup_settings.clear_memoization(:delayed_project_removal)
+ end
+
+ it 'cascades to the application settings value' do
+ expect(delayed_project_removal).to eq(false)
+ end
+ end
+
+ context 'when multiple ancestors set a value' do
+ let(:third_level_subgroup) { create(:group, parent: subgroup) }
+
+ before do
+ group_settings.update!(delayed_project_removal: true)
+ subgroup_settings.update!(delayed_project_removal: false)
+ end
+
+ it 'returns the closest ancestor value' do
+ expect(third_level_subgroup.namespace_settings.delayed_project_removal).to eq(false)
+ end
+ end
+ end
+
+ context 'when parent locks the attribute' do
+ before do
+ subgroup_settings.update!(delayed_project_removal: true)
+ group_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: false)
+
+ subgroup_settings.clear_memoization(:delayed_project_removal)
+ subgroup_settings.clear_memoization(:delayed_project_removal_locked_ancestor)
+ end
+
+ it 'returns the parent value' do
+ expect(delayed_project_removal).to eq(false)
+ end
+
+ it 'does not allow the local value to be saved' do
+ subgroup_settings.delayed_project_removal = nil
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be changed because it is locked by an ancestor/)
+ end
+ end
+
+ context 'when the application settings locks the attribute' do
+ before do
+ subgroup_settings.update!(delayed_project_removal: true)
+ stub_application_setting(lock_delayed_project_removal: true, delayed_project_removal: true)
+ end
+
+ it 'returns the application setting value' do
+ expect(delayed_project_removal).to eq(true)
+ end
+
+ it 'does not allow the local value to be saved' do
+ subgroup_settings.delayed_project_removal = nil
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be changed because it is locked by an ancestor/)
+ end
+ end
+ end
+
+ describe '#delayed_project_removal?' do
+ before do
+ subgroup_settings.update!(delayed_project_removal: true)
+ group_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: false)
+
+ subgroup_settings.clear_memoization(:delayed_project_removal)
+ subgroup_settings.clear_memoization(:delayed_project_removal_locked_ancestor)
+ end
+
+ it 'aliases the method when the attribute is a boolean' do
+ expect(subgroup_settings.delayed_project_removal?).to eq(subgroup_settings.delayed_project_removal)
+ end
+ end
+
+ describe '#delayed_project_removal_locked?' do
+ shared_examples 'not locked' do
+ it 'is not locked by an ancestor' do
+ expect(subgroup_settings.delayed_project_removal_locked_by_ancestor?).to eq(false)
+ end
+
+ it 'is not locked by application setting' do
+ expect(subgroup_settings.delayed_project_removal_locked_by_application_setting?).to eq(false)
+ end
+
+ it 'does not return a locked namespace' do
+ expect(subgroup_settings.delayed_project_removal_locked_ancestor).to be_nil
+ end
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(cascading_namespace_settings: false)
+
+ group_settings.update!(delayed_project_removal: true)
+ end
+
+ it_behaves_like 'not locked'
+ end
+
+ context 'when parent does not lock the attribute' do
+ it_behaves_like 'not locked'
+ end
+
+ context 'when parent locks the attribute' do
+ before do
+ group_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: false)
+
+ subgroup_settings.clear_memoization(:delayed_project_removal)
+ subgroup_settings.clear_memoization(:delayed_project_removal_locked_ancestor)
+ end
+
+ it 'is locked by an ancestor' do
+ expect(subgroup_settings.delayed_project_removal_locked_by_ancestor?).to eq(true)
+ end
+
+ it 'is not locked by application setting' do
+ expect(subgroup_settings.delayed_project_removal_locked_by_application_setting?).to eq(false)
+ end
+
+ it 'returns a locked namespace settings object' do
+ expect(subgroup_settings.delayed_project_removal_locked_ancestor.namespace_id).to eq(group_settings.namespace_id)
+ end
+ end
+
+ context 'when not locked by application settings' do
+ before do
+ stub_application_setting(lock_delayed_project_removal: false)
+ end
+
+ it_behaves_like 'not locked'
+ end
+
+ context 'when locked by application settings' do
+ before do
+ stub_application_setting(lock_delayed_project_removal: true)
+ end
+
+ it 'is not locked by an ancestor' do
+ expect(subgroup_settings.delayed_project_removal_locked_by_ancestor?).to eq(false)
+ end
+
+ it 'is locked by application setting' do
+ expect(subgroup_settings.delayed_project_removal_locked_by_application_setting?).to eq(true)
+ end
+
+ it 'does not return a locked namespace' do
+ expect(subgroup_settings.delayed_project_removal_locked_ancestor).to be_nil
+ end
+ end
+ end
+
+ describe '#lock_delayed_project_removal=' do
+ context 'when parent locks the attribute' do
+ before do
+ group_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: false)
+
+ subgroup_settings.clear_memoization(:delayed_project_removal)
+ subgroup_settings.clear_memoization(:delayed_project_removal_locked_ancestor)
+ end
+
+ it 'does not allow the attribute to be saved' do
+ subgroup_settings.lock_delayed_project_removal = true
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid, /Lock delayed project removal cannot be changed because it is locked by an ancestor/)
+ end
+ end
+
+ context 'when parent does not lock the attribute' do
+ before do
+ group_settings.update!(lock_delayed_project_removal: false)
+
+ subgroup_settings.lock_delayed_project_removal = true
+ end
+
+ it 'allows the lock to be set when the attribute is not nil' do
+ subgroup_settings.delayed_project_removal = true
+
+ expect(subgroup_settings.save).to eq(true)
+ end
+
+ it 'does not allow the lock to be saved when the attribute is nil' do
+ subgroup_settings.delayed_project_removal = nil
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid, /Delayed project removal cannot be nil when locking the attribute/)
+ end
+ end
+
+ context 'when application settings locks the attribute' do
+ before do
+ stub_application_setting(lock_delayed_project_removal: true)
+ end
+
+ it 'does not allow the attribute to be saved' do
+ subgroup_settings.lock_delayed_project_removal = true
+
+ expect { subgroup_settings.save! }
+ .to raise_error(ActiveRecord::RecordInvalid, /Lock delayed project removal cannot be changed because it is locked by an ancestor/)
+ end
+ end
+
+ context 'when application_settings does not lock the attribute' do
+ before do
+ stub_application_setting(lock_delayed_project_removal: false)
+ end
+
+ it 'allows the attribute to be saved' do
+ subgroup_settings.delayed_project_removal = true
+ subgroup_settings.lock_delayed_project_removal = true
+
+ expect(subgroup_settings.save).to eq(true)
+ end
+ end
+ end
+
+ describe 'after update callback' do
+ before do
+ subgroup_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: false)
+ end
+
+ it 'clears descendant locks' do
+ group_settings.update!(lock_delayed_project_removal: true, delayed_project_removal: true)
+
+ expect(subgroup_settings.reload.lock_delayed_project_removal).to eq(false)
+ end
+ end
+end
diff --git a/spec/models/concerns/ci/has_status_spec.rb b/spec/models/concerns/ci/has_status_spec.rb
index ea7dbf0411c..b16420bc658 100644
--- a/spec/models/concerns/ci/has_status_spec.rb
+++ b/spec/models/concerns/ci/has_status_spec.rb
@@ -197,12 +197,6 @@ RSpec.describe Ci::HasStatus do
end
end
- describe '.completed_and_blocked_statuses' do
- subject { Ci::Pipeline.completed_and_blocked_statuses }
-
- it { is_expected.to eq [:success, :failed, :canceled, :skipped, :manual, :scheduled] }
- end
-
context 'for scope with one status' do
shared_examples 'having a job' do |status|
%i[ci_build generic_commit_status].each do |type|
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index 02b266e4fae..71cfe3ff45b 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe ChatMessage::MergeMessage do
project_url: 'http://somewhere.com',
object_attributes: {
- title: "Merge Request title\nSecond line",
+ title: "Merge request title\nSecond line",
id: 10,
iid: 100,
assignee_id: 1,
@@ -35,7 +35,7 @@ RSpec.describe ChatMessage::MergeMessage do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) opened merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>')
+ 'Test User (test.user) opened merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> in <http://somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
end
@@ -46,7 +46,7 @@ RSpec.describe ChatMessage::MergeMessage do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) closed merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>')
+ 'Test User (test.user) closed merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> in <http://somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
end
@@ -60,12 +60,12 @@ RSpec.describe ChatMessage::MergeMessage do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) opened merge request [!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)')
+ 'Test User (test.user) opened merge request [!100 *Merge request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Merge Request opened by Test User (test.user)',
+ title: 'Merge request opened by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
- text: '[!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100)',
+ text: '[!100 *Merge request title*](http://somewhere.com/-/merge_requests/100)',
image: 'http://someavatar.com'
})
end
@@ -78,12 +78,12 @@ RSpec.describe ChatMessage::MergeMessage do
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) closed merge request [!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)')
+ 'Test User (test.user) closed merge request [!100 *Merge request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq({
- title: 'Merge Request closed by Test User (test.user)',
+ title: 'Merge request closed by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
- text: '[!100 *Merge Request title*](http://somewhere.com/-/merge_requests/100)',
+ text: '[!100 *Merge request title*](http://somewhere.com/-/merge_requests/100)',
image: 'http://someavatar.com'
})
end
@@ -97,7 +97,7 @@ RSpec.describe ChatMessage::MergeMessage do
it 'returns a message regarding completed approval of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) approved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'Test User (test.user) approved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
'in <http://somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -110,7 +110,7 @@ RSpec.describe ChatMessage::MergeMessage do
it 'returns a message regarding revocation of completed approval of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) unapproved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'Test User (test.user) unapproved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
'in <http://somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -123,7 +123,7 @@ RSpec.describe ChatMessage::MergeMessage do
it 'returns a message regarding added approval of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) added their approval to merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'Test User (test.user) added their approval to merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
'in <http://somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -136,7 +136,7 @@ RSpec.describe ChatMessage::MergeMessage do
it 'returns a message regarding revoking approval of merge requests' do
expect(subject.pretext).to eq(
- 'Test User (test.user) removed their approval from merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge Request title*> '\
+ 'Test User (test.user) removed their approval from merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
'in <http://somewhere.com|project_name>')
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 84404a31f76..c1a292c9b30 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -170,6 +170,22 @@ RSpec.describe Repository do
end
end
+ describe '#search_branch_names' do
+ subject(:search_branch_names) { repository.search_branch_names('conflict-*') }
+
+ it 'returns matching branch names' do
+ expect(search_branch_names).to contain_exactly(
+ 'conflict-binary-file',
+ 'conflict-resolvable',
+ 'conflict-contains-conflict-markers',
+ 'conflict-missing-side',
+ 'conflict-start',
+ 'conflict-non-utf8',
+ 'conflict-too-large'
+ )
+ end
+ end
+
describe '#list_last_commits_for_tree' do
let(:path_to_commit) do
{
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ac92311e132..ee67afcd50b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2521,32 +2521,12 @@ RSpec.describe User do
describe "#clear_avatar_caches" do
let(:user) { create(:user) }
- context "when :avatar_cache_for_email flag is enabled" do
- before do
- stub_feature_flags(avatar_cache_for_email: true)
- end
-
- it "clears the avatar cache when saving" do
- allow(user).to receive(:avatar_changed?).and_return(true)
-
- expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails)
-
- user.update(avatar: fixture_file_upload('spec/fixtures/dk.png'))
- end
- end
-
- context "when :avatar_cache_for_email flag is disabled" do
- before do
- stub_feature_flags(avatar_cache_for_email: false)
- end
-
- it "doesn't attempt to clear the avatar cache" do
- allow(user).to receive(:avatar_changed?).and_return(true)
+ it "clears the avatar cache when saving" do
+ allow(user).to receive(:avatar_changed?).and_return(true)
- expect(Gitlab::AvatarCache).not_to receive(:delete_by_email)
+ expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails)
- user.update(avatar: fixture_file_upload('spec/fixtures/dk.png'))
- end
+ user.update(avatar: fixture_file_upload('spec/fixtures/dk.png'))
end
end
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index c72fd71884e..36055779a2e 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Ci::PipelineTriggerService do
stub_ci_pipeline_to_return_yaml_file
end
- describe '#execute', :context_aware do
+ describe '#execute' do
let_it_be(:user) { create(:user) }
let(:result) { described_class.new(project, user, params).execute }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 84c38ca0ce2..a3925a0c0fb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -338,20 +338,10 @@ RSpec.configure do |config|
RequestStore.clear!
end
- if ENV['SKIP_RSPEC_CONTEXT_WRAPPING']
- config.around(:example, :context_aware) do |example|
- # Wrap each example in it's own context to make sure the contexts don't
- # leak
- Gitlab::ApplicationContext.with_raw_context { example.run }
- end
- else
- config.around do |example|
- if [:controller, :request, :feature].include?(example.metadata[:type]) || example.metadata[:context_aware]
- Gitlab::ApplicationContext.with_raw_context { example.run }
- else
- example.run
- end
- end
+ config.around do |example|
+ # Wrap each example in it's own context to make sure the contexts don't
+ # leak
+ Gitlab::ApplicationContext.with_raw_context { example.run }
end
config.around do |example|
diff --git a/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb
index c775ca182e6..d5ebda28f0a 100644
--- a/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb
+++ b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'API::CI::Runner application context metadata' do |api_route|
- it 'contains correct context metadata', :context_aware do
+ it 'contains correct context metadata' do
# Avoids popping the context from the thread so we can
# check its content after the request.
allow(Labkit::Context).to receive(:pop)
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 50d50bee727..6b243aef3e6 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -354,33 +354,29 @@ RSpec.shared_examples 'wiki model' do
subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image')
end
- shared_examples 'find_file results' do
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.png')
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
- expect(file.mime_type).to eq('image/png')
- end
+ expect(file.mime_type).to eq('image/png')
+ end
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existent')).to eq(nil)
- end
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existent')).to eq(nil)
+ end
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.png')
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
- expect(file).to be_a Gitlab::Git::WikiFile
- end
+ expect(file).to be_a Gitlab::Git::WikiFile
+ end
- it 'returns the whole file' do
- file = subject.find_file('image.png')
- image.rewind
+ it 'returns the whole file' do
+ file = subject.find_file('image.png')
+ image.rewind
- expect(file.raw_data.b).to eq(image.read.b)
- end
+ expect(file.raw_data.b).to eq(image.read.b)
end
- it_behaves_like 'find_file results'
-
context 'when load_content is disabled' do
it 'includes the file data in the Gitlab::Git::WikiFile' do
file = subject.find_file('image.png', load_content: false)
@@ -388,14 +384,6 @@ RSpec.shared_examples 'wiki model' do
expect(file.raw_data).to be_empty
end
end
-
- context 'when feature flag :gitaly_find_file is disabled' do
- before do
- stub_feature_flags(gitaly_find_file: false)
- end
-
- it_behaves_like 'find_file results'
- end
end
describe '#create_page' do
diff --git a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb
index 57e28e6df57..cb06c9fa596 100644
--- a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'storing arguments in the application context' do
- it 'places the expected params in the application context', :context_aware do
+ it 'places the expected params in the application context' do
# Stub the clearing of the context so we can validate it later
allow(Labkit::Context).to receive(:pop)
diff --git a/spec/views/registrations/welcome/show.html.haml_spec.rb b/spec/views/registrations/welcome/show.html.haml_spec.rb
index d9774582545..639759ae095 100644
--- a/spec/views/registrations/welcome/show.html.haml_spec.rb
+++ b/spec/views/registrations/welcome/show.html.haml_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'registrations/welcome/show' do
- using RSpec::Parameterized::TableSyntax
+ let(:is_gitlab_com) { false }
let_it_be(:user) { User.new }
@@ -13,7 +13,7 @@ RSpec.describe 'registrations/welcome/show' do
allow(view).to receive(:in_trial_flow?).and_return(false)
allow(view).to receive(:user_has_memberships?).and_return(false)
allow(view).to receive(:in_oauth_flow?).and_return(false)
- allow(Gitlab).to receive(:com?).and_return(false)
+ allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
render
end
@@ -22,4 +22,24 @@ RSpec.describe 'registrations/welcome/show' do
it { is_expected.not_to have_selector('label[for="user_setup_for_company"]') }
it { is_expected.to have_button('Get started!') }
+ it { is_expected.to have_selector('input[name="user[email_opted_in]"]') }
+
+ describe 'email opt in' do
+ context 'when on gitlab.com' do
+ let(:is_gitlab_com) { true }
+
+ it 'hides the email-opt in by default' do
+ expect(subject).to have_css('.js-email-opt-in.hidden')
+ end
+ end
+
+ context 'when not on gitlab.com' do
+ let(:is_gitlab_com) { false }
+
+ it 'hides the email-opt in by default' do
+ expect(subject).not_to have_css('.js-email-opt-in.hidden')
+ expect(subject).to have_css('.js-email-opt-in')
+ end
+ end
+ end
end
diff --git a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
index 24143e8cf8a..af4a6dac6cd 100644
--- a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
+++ b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb
@@ -3,45 +3,49 @@
require 'spec_helper'
RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform' do
- context 'when the application setting is enabled' do
+ using RSpec::Parameterized::TableSyntax
+
+ RSpec.shared_examples 'in-product marketing email' do
before do
- stub_application_setting(in_product_marketing_emails_enabled: true)
+ stub_application_setting(in_product_marketing_emails_enabled: in_product_marketing_emails_enabled)
+ stub_experiment(in_product_marketing_emails: experiment_active)
+ allow(::Gitlab).to receive(:com?).and_return(is_gitlab_com)
end
- context 'when the experiment is inactive' do
- before do
- stub_experiment(in_product_marketing_emails: false)
- end
-
- it 'does not execute the in product marketing emails service' do
- expect(Namespaces::InProductMarketingEmailsService).not_to receive(:send_for_all_tracks_and_intervals)
+ it 'executes the email service service' do
+ expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals).exactly(executes_service).times
- subject.perform
- end
+ subject.perform
end
+ end
- context 'when the experiment is active' do
- before do
- stub_experiment(in_product_marketing_emails: true)
- end
+ context 'not on gitlab.com' do
+ let(:is_gitlab_com) { false }
- it 'calls the send_for_all_tracks_and_intervals method on the in product marketing emails service' do
- expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals)
+ where(:in_product_marketing_emails_enabled, :experiment_active, :executes_service) do
+ true | true | 1
+ true | false | 1
+ false | false | 0
+ false | true | 0
+ end
- subject.perform
- end
+ with_them do
+ include_examples 'in-product marketing email'
end
end
- context 'when the application setting is disabled' do
- before do
- stub_application_setting(in_product_marketing_emails_enabled: false)
- end
+ context 'on gitlab.com' do
+ let(:is_gitlab_com) { true }
- it 'does not execute the in product marketing emails service' do
- expect(Namespaces::InProductMarketingEmailsService).not_to receive(:send_for_all_tracks_and_intervals)
+ where(:in_product_marketing_emails_enabled, :experiment_active, :executes_service) do
+ true | true | 1
+ true | false | 0
+ false | false | 0
+ false | true | 0
+ end
- subject.perform
+ with_them do
+ include_examples 'in-product marketing email'
end
end
end
diff --git a/yarn.lock b/yarn.lock
index a0782e2b267..618452b25e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12331,10 +12331,10 @@ vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
-vue-virtual-scroll-list@^1.4.4:
- version "1.4.4"
- resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.4.tgz#5fca7a13f785899bbfb70471ec4fe222437d8495"
- integrity sha512-wU7FDpd9Xy4f62pf8SBg/ak21jMI/pdx4s4JPah+z/zuhmeAafQgp8BjtZvvt+b0BZOsOS1FJuCfUH7azTkivQ==
+vue-virtual-scroll-list@^1.4.7:
+ version "1.4.7"
+ resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.7.tgz#12ee26833885f5bb4d37dc058085ccf3ce5b5a74"
+ integrity sha512-R8bk+k7WMGGoFQ9xF0krGCAlZhQjbJOkDUX+YZD2J+sHQWTzDtmTLS6kiIJToOHK1d/8QPGiD8fd9w0lDP4arg==
vue@^2.6.12:
version "2.6.12"