diff options
104 files changed, 1447 insertions, 336 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index edbbe90a774..5dfcc5715da 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,6 +25,7 @@ default: timeout: 90m workflow: + name: '$PIPELINE_NAME' rules: # If `$FORCE_GITLAB_CI` is set, create a pipeline. - if: '$FORCE_GITLAB_CI' @@ -39,18 +40,23 @@ workflow: - if: '($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/' variables: RUBY_VERSION: "3.0" + PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline' # For merge requests running exclusively in Ruby 3.0 - if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/' variables: RUBY_VERSION: "3.0" + PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline' # For (detached) merge request pipelines. - if: '$CI_MERGE_REQUEST_IID' + variables: + PIPELINE_NAME: '$CI_MERGE_REQUEST_EVENT_TYPE MR pipeline' # For the scheduled pipelines, we set specific variables. - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"' variables: CRYSTALBALL: "true" CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true" NOTIFY_PIPELINE_FAILURE_CHANNEL: "master-broken" + PIPELINE_NAME: 'Scheduled $CI_COMMIT_BRANCH pipeline' # Run pipelines for ruby3 branch - if: '$CI_COMMIT_BRANCH == "ruby3" && $CI_PIPELINE_SOURCE == "schedule"' variables: @@ -58,6 +64,7 @@ workflow: NOTIFY_PIPELINE_FAILURE_CHANNEL: "f_ruby3" OMNIBUS_GITLAB_RUBY3_BUILD: "true" OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY3" + PIPELINE_NAME: 'Scheduled ruby 3 pipeline' # This work around https://gitlab.com/gitlab-org/gitlab/-/issues/332411 whichs prevents usage of dependency proxy # when pipeline is triggered by a project access token. - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $GITLAB_USER_LOGIN =~ /project_\d+_bot\d*/' diff --git a/.rubocop_todo/layout/space_inside_parens.yml b/.rubocop_todo/layout/space_inside_parens.yml index 3e443d9d5d1..2c0d8b377e7 100644 --- a/.rubocop_todo/layout/space_inside_parens.yml +++ b/.rubocop_todo/layout/space_inside_parens.yml @@ -1,9 +1,7 @@ --- # Cop supports --autocorrect. Layout/SpaceInsideParens: - # Offense count: 422 - # Temporarily disabled due to too many offenses - Enabled: false + Details: grace period Exclude: - 'db/post_migrate/20210722042939_update_issuable_slas_where_issue_closed.rb' - 'ee/app/models/ee/dependency_proxy/blob.rb' @@ -11,6 +9,7 @@ Layout/SpaceInsideParens: - 'ee/lib/ee/gitlab/auth/ldap/access.rb' - 'ee/lib/gitlab/auth/smartcard/session.rb' - 'ee/lib/system_check/geo/current_node_check.rb' + - 'ee/spec/controllers/projects/mirrors_controller_spec.rb' - 'ee/spec/finders/ee/alert_management/http_integrations_finder_spec.rb' - 'ee/spec/finders/epics_finder_spec.rb' - 'ee/spec/finders/security/findings_finder_spec.rb' @@ -31,6 +30,7 @@ Layout/SpaceInsideParens: - 'ee/spec/models/ee/iteration_spec.rb' - 'ee/spec/models/ee/iterations/cadence_spec.rb' - 'ee/spec/models/ee/key_spec.rb' + - 'ee/spec/models/ee/project_setting_spec.rb' - 'ee/spec/models/ee/system_note_metadata_spec.rb' - 'ee/spec/models/geo/every_geo_event_spec.rb' - 'ee/spec/models/incident_management/escalation_rule_spec.rb' @@ -87,21 +87,6 @@ Layout/SpaceInsideParens: - 'ee/spec/support/shared_examples/services/geo/geo_request_service_shared_examples.rb' - 'ee/spec/workers/elastic/migration_worker_spec.rb' - 'ee/spec/workers/security/auto_fix_worker_spec.rb' - - 'lib/backup/files.rb' - - 'lib/gitlab/ci/reports/security/finding.rb' - - 'lib/gitlab/ci/runner_instructions.rb' - - 'lib/gitlab/database/partitioning/single_numeric_list_partition.rb' - - 'lib/gitlab/database/postgres_hll/buckets.rb' - - 'lib/gitlab/diff/parser.rb' - - 'lib/gitlab/gitaly_client/commit_service.rb' - - 'lib/gitlab/import_export/decompressed_archive_size_validator.rb' - - 'lib/gitlab/memory/watchdog/monitor_state.rb' - - 'lib/gitlab/prometheus_client.rb' - - 'lib/gitlab/sidekiq_daemon/memory_killer.rb' - - 'lib/gitlab/tracking/incident_management.rb' - - 'lib/gitlab/visibility_level.rb' - - 'lib/security/ci_configuration/sast_build_action.rb' - - 'lib/tasks/gitlab/sidekiq.rake' - 'qa/qa/page/group/settings/group_deploy_tokens.rb' - 'qa/qa/specs/features/ee/browser_ui/10_govern/scan_result_policy_vulnerabilities_spec.rb' - 'qa/qa/tools/delete_subgroups.rb' @@ -205,6 +190,8 @@ Layout/SpaceInsideParens: - 'spec/mailers/emails/profile_spec.rb' - 'spec/migrations/20211130165043_backfill_sequence_column_for_sprints_table_spec.rb' - 'spec/migrations/backfill_issues_upvotes_count_spec.rb' + - 'spec/models/ci/pending_build_spec.rb' + - 'spec/models/ci/running_build_spec.rb' - 'spec/models/ml/candidate_metric_spec.rb' - 'spec/models/ml/candidate_spec.rb' - 'spec/policies/clusters/agent_policy_spec.rb' @@ -358,7 +358,7 @@ gem 'prometheus-client-mmap', '~> 0.16', require: 'prometheus/client' gem 'warning', '~> 1.3.0' group :development do - gem 'lefthook', '~> 1.2.2', require: false + gem 'lefthook', '~> 1.2.4', require: false gem 'rubocop' gem 'solargraph', '~> 0.47.2', require: false diff --git a/Gemfile.checksum b/Gemfile.checksum index 2dcf8cd9b5f..4d4d4a61329 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -312,7 +312,7 @@ {"name":"kramdown-parser-gfm","version":"1.1.0","platform":"ruby","checksum":"fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729"}, {"name":"kubeclient","version":"4.9.3","platform":"ruby","checksum":"d5d38e719fbac44f396851aa57cd1b9f4f7dab4410ab680ccd21c9b741230046"}, {"name":"launchy","version":"2.5.0","platform":"ruby","checksum":"954243c4255920982ce682f89a42e76372dba94770bf09c23a523e204bdebef5"}, -{"name":"lefthook","version":"1.2.2","platform":"ruby","checksum":"51359198401f1d6ccd4d21ce974eadad871d725f6de08fddb2f441512e34049f"}, +{"name":"lefthook","version":"1.2.4","platform":"ruby","checksum":"9cca2d00f9363c308219ba040da3e90dc5c059e86e43cf9a5735ef7b829801bf"}, {"name":"letter_opener","version":"1.7.0","platform":"ruby","checksum":"095bc0d58e006e5b43ea7d219e64ecf2de8d1f7d9dafc432040a845cf59b4725"}, {"name":"letter_opener_web","version":"2.0.0","platform":"ruby","checksum":"33860ad41e1785d75456500e8ca8bba8ed71ee6eaf08a98d06bbab67c5577b6f"}, {"name":"libyajl2","version":"1.2.0","platform":"ruby","checksum":"1117cd1e48db013b626e36269bbf1cef210538ca6d2e62d3fa3db9ded005b258"}, diff --git a/Gemfile.lock b/Gemfile.lock index 986110378c7..92e3527e871 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -832,7 +832,7 @@ GEM rest-client (~> 2.0) launchy (2.5.0) addressable (~> 2.7) - lefthook (1.2.2) + lefthook (1.2.4) letter_opener (1.7.0) launchy (~> 2.2) letter_opener_web (2.0.0) @@ -1714,7 +1714,7 @@ DEPENDENCIES knapsack (~> 1.21.1) kramdown (~> 2.3.1) kubeclient (~> 4.9.3) - lefthook (~> 1.2.2) + lefthook (~> 1.2.4) letter_opener_web (~> 2.0.0) license_finder (~> 7.0) licensee (~> 9.15) diff --git a/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue new file mode 100644 index 00000000000..4638ba8a268 --- /dev/null +++ b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue @@ -0,0 +1,160 @@ +<script> +import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { glEmojiTag } from '~/emoji'; +import { s__, sprintf } from '~/locale'; +import Tracking from '~/tracking'; +import { getHideAlertModalCookie, setHideAlertModalCookie } from '../utils'; +import { + UPGRADE_DOCS_URL, + ABOUT_RELEASES_PAGE, + ALERT_MODAL_ID, + TRACKING_ACTIONS, + TRACKING_LABELS, +} from '../constants'; + +export default { + name: 'SecurityPatchUpgradeAlertModal', + i18n: { + modalTitle: s__('VersionCheck|Important notice - Critical security release'), + modalBodyNoStableVersions: s__( + 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation immediately.', + ), + modalBodyStableVersions: s__( + 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation to one of the following versions immediately: %{latestStableVersions}.', + ), + modalDetails: s__('VersionCheck|%{details}'), + learnMore: s__('VersionCheck|Learn more about this critical security release.'), + primaryButtonText: s__('VersionCheck|Upgrade now'), + secondaryButtonText: s__('VersionCheck|Remind me again in 3 days'), + }, + components: { + GlModal, + GlSprintf, + GlLink, + GlButton, + }, + directives: { + SafeHtml, + }, + mixins: [Tracking.mixin()], + props: { + currentVersion: { + type: String, + required: true, + }, + details: { + type: String, + required: false, + default: '', + }, + latestStableVersions: { + type: Array, + required: false, + default: () => [], + }, + }, + data() { + return { + visible: true, + }; + }, + computed: { + alertEmoji() { + return glEmojiTag('rotating_light'); + }, + modalBody() { + if (this.latestStableVersions?.length > 0) { + return this.$options.i18n.modalBodyStableVersions; + } + + return this.$options.i18n.modalBodyNoStableVersions; + }, + modalDetails() { + return sprintf(this.$options.i18n.modalDetails, { details: this.details }); + }, + latestStableVersionsStrings() { + return this.latestStableVersions?.length > 0 ? this.latestStableVersions.join(', ') : ''; + }, + }, + created() { + if (getHideAlertModalCookie(this.currentVersion)) { + this.visible = false; + return; + } + + this.dispatchTrackingEvent(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL); + }, + methods: { + dispatchTrackingEvent(action, label) { + this.track(action, { + label, + property: this.currentVersion, + }); + }, + trackLearnMoreClick() { + this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK); + }, + trackRemindMeLaterClick() { + this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN); + setHideAlertModalCookie(this.currentVersion); + this.$refs.alertModal.hide(); + }, + trackUpgradeNowClick() { + this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK); + setHideAlertModalCookie(this.currentVersion); + }, + trackModalDismissed() { + this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS); + }, + }, + safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, + UPGRADE_DOCS_URL, + ABOUT_RELEASES_PAGE, + ALERT_MODAL_ID, +}; +</script> + +<template> + <gl-modal + ref="alertModal" + :modal-id="$options.ALERT_MODAL_ID" + :visible="visible" + @close="trackModalDismissed" + > + <template #modal-title> + <span v-safe-html:[$options.safeHtmlConfig]="alertEmoji"></span> + <span data-testid="alert-modal-title">{{ $options.i18n.modalTitle }}</span> + </template> + <template #default> + <div data-testid="alert-modal-body" class="gl-mb-6"> + <gl-sprintf :message="modalBody"> + <template #currentVersion> + <span class="gl-font-weight-bold">{{ currentVersion }}</span> + </template> + <template #latestStableVersions> + <span class="gl-font-weight-bold">{{ latestStableVersionsStrings }}</span> + </template> + </gl-sprintf> + </div> + <div v-if="details" data-testid="alert-modal-details" class="gl-mb-6"> + {{ modalDetails }} + </div> + <gl-link :href="$options.ABOUT_RELEASES_PAGE" @click="trackLearnMoreClick">{{ + $options.i18n.learnMore + }}</gl-link> + </template> + <template #modal-footer> + <gl-button data-testid="alert-modal-remind-button" @click="trackRemindMeLaterClick">{{ + $options.i18n.secondaryButtonText + }}</gl-button> + <gl-button + data-testid="alert-modal-upgrade-button" + :href="$options.UPGRADE_DOCS_URL" + variant="confirm" + @click="trackUpgradeNowClick" + >{{ $options.i18n.primaryButtonText }}</gl-button + > + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/gitlab_version_check/constants.js b/app/assets/javascripts/gitlab_version_check/constants.js index 43759c79b5b..049397148ab 100644 --- a/app/assets/javascripts/gitlab_version_check/constants.js +++ b/app/assets/javascripts/gitlab_version_check/constants.js @@ -9,3 +9,23 @@ export const STATUS_TYPES = { export const UPGRADE_DOCS_URL = helpPagePath('update/index'); export const ABOUT_RELEASES_PAGE = 'https://about.gitlab.com/releases/categories/releases/'; + +export const ALERT_MODAL_ID = 'security-patch-upgrade-alert-modal'; + +export const COOKIE_EXPIRATION = 3; + +export const COOKIE_SUFFIX = '-hide-alert-modal'; + +export const TRACKING_ACTIONS = { + RENDER: 'render', + CLICK_LINK: 'click_link', + CLICK_BUTTON: 'click_button', +}; + +export const TRACKING_LABELS = { + MODAL: 'security_patch_upgrade_alert_modal', + LEARN_MORE_LINK: 'security_patch_upgrade_alert_modal_learn_more', + REMIND_ME_BTN: 'security_patch_upgrade_alert_modal_remind_3_days', + UPGRADE_BTN_LINK: 'security_patch_upgrade_alert_modal_upgrade_now', + DISMISS: 'security_patch_upgrade_alert_modal_close', +}; diff --git a/app/assets/javascripts/gitlab_version_check/index.js b/app/assets/javascripts/gitlab_version_check/index.js index b3180c2d1ba..edb7e9abe49 100644 --- a/app/assets/javascripts/gitlab_version_check/index.js +++ b/app/assets/javascripts/gitlab_version_check/index.js @@ -1,7 +1,8 @@ import Vue from 'vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import GitlabVersionCheckBadge from './components/gitlab_version_check_badge.vue'; import SecurityPatchUpgradeAlert from './components/security_patch_upgrade_alert.vue'; +import SecurityPatchUpgradeAlertModal from './components/security_patch_upgrade_alert_modal.vue'; const mountGitlabVersionCheckBadge = (el) => { const { size, version } = el.dataset; @@ -51,16 +52,46 @@ const mountSecurityPatchUpgradeAlert = (el) => { } }; +const mountSecurityPatchUpgradeAlertModal = (el) => { + const { currentVersion, version } = el.dataset; + + try { + const { details, latestStableVersions } = convertObjectPropsToCamelCase(JSON.parse(version)); + + return new Vue({ + el, + render(createElement) { + return createElement(SecurityPatchUpgradeAlertModal, { + props: { + currentVersion, + details, + latestStableVersions, + }, + }); + }, + }); + } catch { + return null; + } +}; + export default () => { const renderedApps = []; const securityPatchUpgradeAlert = document.getElementById('js-security-patch-upgrade-alert'); + const securityPatchUpgradeAlertModal = document.getElementById( + 'js-security-patch-upgrade-alert-modal', + ); const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')]; if (securityPatchUpgradeAlert) { renderedApps.push(mountSecurityPatchUpgradeAlert(securityPatchUpgradeAlert)); } + if (securityPatchUpgradeAlertModal) { + renderedApps.push(mountSecurityPatchUpgradeAlertModal(securityPatchUpgradeAlertModal)); + } + renderedApps.push(...versionCheckBadges.map((el) => mountGitlabVersionCheckBadge(el))); return renderedApps; diff --git a/app/assets/javascripts/gitlab_version_check/utils.js b/app/assets/javascripts/gitlab_version_check/utils.js new file mode 100644 index 00000000000..d2f4349483c --- /dev/null +++ b/app/assets/javascripts/gitlab_version_check/utils.js @@ -0,0 +1,18 @@ +import { setCookie, getCookie, parseBoolean } from '~/lib/utils/common_utils'; +import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from './constants'; + +const buildKey = (currentVersion) => { + return `${currentVersion}${COOKIE_SUFFIX}`; +}; + +export const setHideAlertModalCookie = (currentVersion) => { + const key = buildKey(currentVersion); + + setCookie(key, true, { expires: COOKIE_EXPIRATION }); +}; + +export const getHideAlertModalCookie = (currentVersion) => { + const key = buildKey(currentVersion); + + return parseBoolean(getCookie(key)); +}; diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index c871489fc07..376d71c7b31 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -460,19 +460,17 @@ export default { :work-item-type="workItemType" @error="updateError = $event" /> - <template v-if="workItemsMvcEnabled"> - <work-item-milestone - v-if="workItemMilestone" - :work-item-id="workItem.id" - :work-item-milestone="workItemMilestone.milestone" - :work-item-type="workItemType" - :fetch-by-iid="fetchByIid" - :query-variables="queryVariables" - :can-update="canUpdate" - :full-path="fullPath" - @error="updateError = $event" - /> - </template> + <work-item-milestone + v-if="workItemMilestone" + :work-item-id="workItem.id" + :work-item-milestone="workItemMilestone.milestone" + :work-item-type="workItemType" + :fetch-by-iid="fetchByIid" + :query-variables="queryVariables" + :can-update="canUpdate" + :full-path="fullPath" + @error="updateError = $event" + /> <work-item-weight v-if="workItemWeight" class="gl-mb-5" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index 4fcdd19784d..65855d58d67 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -122,7 +122,7 @@ export default { confidential: this.parentConfidential, }; - if (this.associateMilestone) { + if (this.parentMilestoneId) { workItemInput = { ...workItemInput, milestoneWidget: { @@ -167,9 +167,6 @@ export default { parentMilestoneId() { return this.parentMilestone?.id; }, - associateMilestone() { - return this.parentMilestoneId && this.workItemsMvcEnabled; - }, isSubmitButtonDisabled() { return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0; }, diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb index 42edc328fa1..b01320ce3ec 100644 --- a/app/controllers/concerns/product_analytics_tracking.rb +++ b/app/controllers/concerns/product_analytics_tracking.rb @@ -64,30 +64,32 @@ module ProductAnalyticsTracking def event_enabled?(event) events_to_ff = { - g_analytics_valuestream: :route_hll_to_snowplow, - - i_search_paid: :route_hll_to_snowplow_phase2, - i_search_total: :route_hll_to_snowplow_phase2, - i_search_advanced: :route_hll_to_snowplow_phase2, - i_ecosystem_jira_service_list_issues: :route_hll_to_snowplow_phase2, - users_viewing_analytics_group_devops_adoption: :route_hll_to_snowplow_phase2, - i_analytics_dev_ops_adoption: :route_hll_to_snowplow_phase2, - i_analytics_dev_ops_score: :route_hll_to_snowplow_phase2, - p_analytics_merge_request: :route_hll_to_snowplow_phase2, - i_analytics_instance_statistics: :route_hll_to_snowplow_phase2, - g_analytics_contribution: :route_hll_to_snowplow_phase2, - p_analytics_pipelines: :route_hll_to_snowplow_phase2, - p_analytics_code_reviews: :route_hll_to_snowplow_phase2, - p_analytics_valuestream: :route_hll_to_snowplow_phase2, - p_analytics_insights: :route_hll_to_snowplow_phase2, - p_analytics_issues: :route_hll_to_snowplow_phase2, - p_analytics_repo: :route_hll_to_snowplow_phase2, - g_analytics_insights: :route_hll_to_snowplow_phase2, - g_analytics_issues: :route_hll_to_snowplow_phase2, - g_analytics_productivity: :route_hll_to_snowplow_phase2, - i_analytics_cohorts: :route_hll_to_snowplow_phase2 + g_analytics_valuestream: '', + + i_search_paid: :_phase2, + i_search_total: :_phase2, + i_search_advanced: :_phase2, + i_ecosystem_jira_service_list_issues: :_phase2, + users_viewing_analytics_group_devops_adoption: :_phase2, + i_analytics_dev_ops_adoption: :_phase2, + i_analytics_dev_ops_score: :_phase2, + p_analytics_merge_request: :_phase2, + i_analytics_instance_statistics: :_phase2, + g_analytics_contribution: :_phase2, + p_analytics_pipelines: :_phase2, + p_analytics_code_reviews: :_phase2, + p_analytics_valuestream: :_phase2, + p_analytics_insights: :_phase2, + p_analytics_issues: :_phase2, + p_analytics_repo: :_phase2, + g_analytics_insights: :_phase2, + g_analytics_issues: :_phase2, + g_analytics_productivity: :_phase2, + i_analytics_cohorts: :_phase2, + + g_compliance_dashboard: :_phase4 } - Feature.enabled?(events_to_ff[event.to_sym], tracking_namespace_source) + Feature.enabled?("route_hll_to_snowplow#{events_to_ff[event.to_sym]}", tracking_namespace_source) end end diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb index 42f4f99d4a9..a3de875c196 100644 --- a/app/graphql/resolvers/work_items_resolver.rb +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -55,6 +55,7 @@ module Resolvers last_edited_by: :last_edited_by, assignees: :assignees, parent: :work_item_parent, + children: { work_item_children: [:author, { project: :project_feature }] }, labels: :labels, milestone: :milestone } diff --git a/app/graphql/types/ci/freeze_period_status_enum.rb b/app/graphql/types/ci/freeze_period_status_enum.rb new file mode 100644 index 00000000000..aebd0f537e9 --- /dev/null +++ b/app/graphql/types/ci/freeze_period_status_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Ci + class FreezePeriodStatusEnum < BaseEnum + graphql_name 'CiFreezePeriodStatus' + description 'Deploy freeze period status' + + value 'ACTIVE', value: :active, description: 'Freeze period is active.' + value 'INACTIVE', value: :inactive, description: 'Freeze period is inactive.' + end + end +end diff --git a/app/graphql/types/ci/freeze_period_type.rb b/app/graphql/types/ci/freeze_period_type.rb new file mode 100644 index 00000000000..c1d0e9ce3b4 --- /dev/null +++ b/app/graphql/types/ci/freeze_period_type.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Types + module Ci + class FreezePeriodType < BaseObject + graphql_name 'CiFreezePeriod' + description 'Represents a deployment freeze window of a project' + + authorize :read_freeze_period + + present_using ::Ci::FreezePeriodPresenter + + field :status, Types::Ci::FreezePeriodStatusEnum, + description: 'Freeze period status.', + null: false + + field :start_cron, GraphQL::Types::String, + description: 'Start of the freeze period in cron format.', + null: false, + method: :freeze_start + + field :end_cron, GraphQL::Types::String, + description: 'End of the freeze period in cron format.', + null: false, + method: :freeze_end + + field :start_time, Types::TimeType, + description: 'Timestamp (UTC) of when the current/next active period starts.', + null: true + + field :end_time, Types::TimeType, + description: 'Timestamp (UTC) of when the current/next active period ends.', + null: true, + method: :time_end_from_now + end + end +end diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 2249fa49f06..5f58fc38540 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -73,6 +73,11 @@ module Types description: 'Last deployment of the environment.', resolver: Resolvers::Environments::LastDeploymentResolver + field :deploy_freezes, + [Types::Ci::FreezePeriodType], + null: true, + description: 'Deployment freeze periods of the environment.' + def tier object.tier.to_sym end diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb index 018e7298da5..4ec8ec84779 100644 --- a/app/graphql/types/work_items/widgets/hierarchy_type.rb +++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb @@ -39,7 +39,10 @@ module Types alias_method :has_children, :has_children? def children - object.children.inc_relations_for_permission_check + relation = object.children + relation = relation.inc_relations_for_permission_check unless object.children.loaded? + + relation end end # rubocop:enable Graphql/AuthorizeTypes diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb index da0bbbacddd..1bf32e04a15 100644 --- a/app/models/ci/freeze_period.rb +++ b/app/models/ci/freeze_period.rb @@ -4,6 +4,10 @@ module Ci class FreezePeriod < Ci::ApplicationRecord include StripAttribute include Ci::NamespacedModelName + include Gitlab::Utils::StrongMemoize + + STATUS_ACTIVE = :active + STATUS_INACTIVE = :inactive default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope @@ -14,5 +18,60 @@ module Ci validates :freeze_start, cron: true, presence: true validates :freeze_end, cron: true, presence: true validates :cron_timezone, cron_freeze_period_timezone: true, presence: true + + def active? + status == STATUS_ACTIVE + end + + def status + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:status") do + within_freeze_period? ? STATUS_ACTIVE : STATUS_INACTIVE + end + end + + def time_start + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_start") do + freeze_start_parsed_cron.previous_time_from(time_zone_now) + end + end + + def next_time_start + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:next_time_start") do + freeze_start_parsed_cron.next_time_from(time_zone_now) + end + end + + def time_end_from_now + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_now") do + freeze_end_parsed_cron.next_time_from(time_zone_now) + end + end + + def time_end_from_start + Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_start") do + freeze_end_parsed_cron.next_time_from(time_start) + end + end + + private + + def within_freeze_period? + time_start <= time_zone_now && time_zone_now <= time_end_from_start + end + + def freeze_start_parsed_cron + Gitlab::Ci::CronParser.new(freeze_start, cron_timezone) + end + strong_memoize_attr :freeze_start_parsed_cron + + def freeze_end_parsed_cron + Gitlab::Ci::CronParser.new(freeze_end, cron_timezone) + end + strong_memoize_attr :freeze_end_parsed_cron + + def time_zone_now + Time.zone.now + end + strong_memoize_attr :time_zone_now end end diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb deleted file mode 100644 index e810bb3f229..00000000000 --- a/app/models/ci/freeze_period_status.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module Ci - class FreezePeriodStatus - attr_reader :project - - def initialize(project:) - @project = project - end - - def execute - project.freeze_periods.any? { |period| within_freeze_period?(period) } - end - - def within_freeze_period?(period) - start_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone) - end_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone) - - start_freeze = start_freeze_cron.previous_time_from(time_zone_now) - end_freeze = end_freeze_cron.next_time_from(start_freeze) - - start_freeze <= time_zone_now && time_zone_now <= end_freeze - end - - private - - def time_zone_now - @time_zone_now ||= Time.zone.now - end - end -end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 181a2bbbb98..632fd0fd24d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -725,7 +725,7 @@ module Ci def freeze_period? strong_memoize(:freeze_period) do - Ci::FreezePeriodStatus.new(project: project).execute + project.freeze_periods.any?(&:active?) end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 684226907d4..a5e958a0645 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -507,6 +507,12 @@ class Environment < ApplicationRecord environment_type.nil? end + def deploy_freezes + Gitlab::SafeRequestStore.fetch("project:#{project_id}:freeze_periods_for_environments") do + project.freeze_periods + end + end + private # We deliberately avoid using AddressableUrlValidator to allow users to update their environments even if they have diff --git a/app/models/project.rb b/app/models/project.rb index b250fe87fdb..65785e81f1b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -89,23 +89,21 @@ class Project < ApplicationRecord cache_markdown_field :description, pipeline: :description - default_value_for :packages_enabled, true - default_value_for :archived, false - default_value_for :resolve_outdated_diff_discussions, false - default_value_for(:repository_storage) do - Repository.pick_storage_shard - end + attribute :packages_enabled, default: true + attribute :archived, default: false + attribute :resolve_outdated_diff_discussions, default: false + attribute :repository_storage, default: -> { Repository.pick_storage_shard } + attribute :shared_runners_enabled, default: -> { Gitlab::CurrentSettings.shared_runners_enabled } + attribute :only_allow_merge_if_all_discussions_are_resolved, default: false + attribute :remove_source_branch_after_merge, default: true + attribute :autoclose_referenced_issues, default: true + attribute :ci_config_path, default: -> { Gitlab::CurrentSettings.default_ci_config_path } - default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled } default_value_for :issues_enabled, gitlab_config_features.issues default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :snippets_enabled, gitlab_config_features.snippets - default_value_for :only_allow_merge_if_all_discussions_are_resolved, false - default_value_for :remove_source_branch_after_merge, true - default_value_for :autoclose_referenced_issues, true - default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path } add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required }, diff --git a/app/models/work_item.rb b/app/models/work_item.rb index ed6f9d161a6..0810c520f7e 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -38,6 +38,18 @@ class WorkItem < Issue end end + def ancestors + hierarchy.ancestors(hierarchy_order: :asc) + end + + def same_type_base_and_ancestors + hierarchy(same_type: true).base_and_ancestors(hierarchy_order: :asc) + end + + def same_type_descendants_depth + hierarchy(same_type: true).max_descendants_depth.to_i + end + private override :parent_link_confidentiality @@ -56,6 +68,13 @@ class WorkItem < Issue Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author) end + + def hierarchy(options = {}) + base = self.class.where(id: id) + base = base.where(work_item_type_id: work_item_type_id) if options[:same_type] + + ::Gitlab::WorkItems::WorkItemHierarchy.new(base, options: options) + end end WorkItem.prepend_mod diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb index 9df2f026bd0..875e6495587 100644 --- a/app/models/work_items/parent_link.rb +++ b/app/models/work_items/parent_link.rb @@ -15,6 +15,7 @@ module WorkItems validate :validate_child_type validate :validate_parent_type validate :validate_hierarchy_restrictions + validate :validate_cyclic_reference validate :validate_same_project validate :validate_max_children validate :validate_confidentiality @@ -98,6 +99,31 @@ module WorkItems if restriction.nil? errors.add :work_item, _('is not allowed to add this type of parent') + return + end + + validate_depth(restriction.maximum_depth) + end + + def validate_depth(depth) + return unless depth + return if work_item.work_item_type_id != work_item_parent.work_item_type_id + + if work_item_parent.same_type_base_and_ancestors.count + work_item.same_type_descendants_depth > depth + errors.add :work_item, _('reached maximum depth') + end + end + + def validate_cyclic_reference + return unless db_restrictions_enabled? + return unless work_item_parent&.id && work_item&.id + + if work_item.id == work_item_parent.id + errors.add :work_item, _('is not allowed to point to itself') + end + + if work_item_parent.ancestors.detect { |ancestor| work_item.id == ancestor.id } + errors.add :work_item, _('is already present in ancestors') end end end diff --git a/app/policies/ci/freeze_period_policy.rb b/app/policies/ci/freeze_period_policy.rb index 60e53a7b2f9..9e2cca5e5a2 100644 --- a/app/policies/ci/freeze_period_policy.rb +++ b/app/policies/ci/freeze_period_policy.rb @@ -2,6 +2,6 @@ module Ci class FreezePeriodPolicy < BasePolicy - delegate { @subject.resource_parent } + delegate { @subject.project } end end diff --git a/app/presenters/ci/freeze_period_presenter.rb b/app/presenters/ci/freeze_period_presenter.rb new file mode 100644 index 00000000000..064197f34dd --- /dev/null +++ b/app/presenters/ci/freeze_period_presenter.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ci + class FreezePeriodPresenter < Gitlab::View::Presenter::Delegated + presents ::Ci::FreezePeriod, as: :freeze_period + + def start_time + return freeze_period.time_start if freeze_period.active? + + freeze_period.next_time_start + end + end +end diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index f6635ad17ef..9cfc3100078 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -1,6 +1,6 @@ .settings-content = gitlab_ui_form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' } do |f| - = form_errors(@application_setting ) + = form_errors(@application_setting) %fieldset .form-group diff --git a/app/views/admin/application_settings/_default_branch.html.haml b/app/views/admin/application_settings/_default_branch.html.haml index 7be4bac02fd..67de5ffb2b9 100644 --- a/app/views/admin/application_settings/_default_branch.html.haml +++ b/app/views/admin/application_settings/_default_branch.html.haml @@ -8,7 +8,7 @@ = f.label :default_branch_name, _('Initial default branch name'), class: 'label-light' = f.text_field :default_branch_name, placeholder: Gitlab::DefaultBranch.value, class: 'form-control gl-form-input' %span.form-text.text-muted - = (s_("AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name } ).html_safe + = (s_("AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name }).html_safe = render 'shared/default_branch_protection', f: f diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml index 3e7803f59e5..d6860cc08ac 100644 --- a/app/views/admin/application_settings/service_usage_data.html.haml +++ b/app/views/admin/application_settings/service_usage_data.html.haml @@ -10,10 +10,10 @@ %h3= name - if @service_ping_data_present - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } } ) do + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } }) do = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true) %span.js-text.gl-display-inline= _('Preview payload') - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } } ) do + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } }) do = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true) %span.js-text.gl-display-inline= _('Download payload') %pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index dfd3b87c674..7699445740a 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -17,14 +17,14 @@ = f.label :broadcast_type, _('Type') .col-sm-10 = f.select :broadcast_type, broadcast_type_options, {}, class: 'form-control js-broadcast-message-type' - .form-group.row.js-broadcast-message-background-color-form-group{ class: ('hidden' unless @broadcast_message.banner? ) } + .form-group.row.js-broadcast-message-background-color-form-group{ class: ('hidden' unless @broadcast_message.banner?) } .col-sm-2.col-form-label = f.label :theme, _("Theme") .col-sm-10 .input-group = f.select :theme, broadcast_theme_options, {}, class: 'form-control js-broadcast-message-theme' - .form-group.row.js-broadcast-message-dismissable-form-group{ class: ('hidden' unless @broadcast_message.banner? ) } + .form-group.row.js-broadcast-message-dismissable-form-group{ class: ('hidden' unless @broadcast_message.banner?) } .col-sm-2.col-form-label.pt-0 = f.label :starts_at, _("Dismissable") .col-sm-10 diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index 3121cd2ae59..a24cd000464 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -14,11 +14,11 @@ icon: 'pencil', button_options: { title: _('Edit'), 'aria-label' => _('Edit'), - class: button_classes } ) + class: button_classes }) = render Pajamas::ButtonComponent.new(category: :tertiary, href: url_for([:admin, @user, identity]), icon: 'remove', button_options: { title: _('Delete'), 'aria-label' => _('Delete identity'), class: button_classes, - data: { method: :delete, confirm: _("Are you sure you want to remove this identity?") } } ) + data: { method: :delete, confirm: _("Are you sure you want to remove this identity?") } }) diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml index c7c30673d74..b6a97887b5c 100644 --- a/app/views/admin/projects/_projects.html.haml +++ b/app/views/admin/projects/_projects.html.haml @@ -26,7 +26,7 @@ .controls.gl-flex-shrink-0.gl-ml-5 = render Pajamas::ButtonComponent.new(href: edit_project_path(project), button_options: { id: dom_id(project, :edit) }) do = s_('Edit') - = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } } ) do + = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } }) do = s_('AdminProjects|Delete') = paginate @projects, theme: 'gitlab' diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml index f6d05959d2e..efd2e53e100 100644 --- a/app/views/groups/registry/repositories/index.html.haml +++ b/app/views/groups/registry/repositories/index.html.haml @@ -1,6 +1,6 @@ - page_title _("Container Registry") - @content_class = "limit-container-width" unless fluid_layout -- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil} ) +- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil}) %section #js-container-registry{ data: { endpoint: group_container_registries_path(@group), diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml index 1146063969b..9ea83397348 100644 --- a/app/views/groups/runners/index.html.haml +++ b/app/views/groups/runners/index.html.haml @@ -1,3 +1,3 @@ - page_title s_('Runners|Runners') -#js-group-runners{ data: group_runners_data_attributes(@group).merge( { group_runners_limited_count: @group_runners_limited_count, registration_token: @group_runner_registration_token } ) } +#js-group-runners{ data: group_runners_data_attributes(@group).merge({ group_runners_limited_count: @group_runners_limited_count, registration_token: @group_runner_registration_token }) } diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 351d603ece0..079123e989e 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -21,7 +21,7 @@ = file_field_tag :file, class: '' .row .form-actions.col-sm-12 - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { data: { qa_selector: 'import_project_button' }}) do + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { qa_selector: 'import_project_button' }}) do = _('Import project') = render Pajamas::ButtonComponent.new(href: new_project_path) do = _('Cancel') diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 8815dec5a6b..717175e8eb3 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -14,7 +14,7 @@ %span.nav-item-name = _('Overview') %ul.sidebar-sub-level-items - = nav_link(controller: %w[dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts], html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: %w[dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts], html_options: { class: "fly-out-top-item" }) do = link_to admin_root_path do %strong.fly-out-top-item-name = _('Overview') @@ -82,7 +82,7 @@ = _('Monitoring') %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } } - = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" }) do = link_to admin_system_info_path do %strong.fly-out-top-item-name = _('Monitoring') @@ -117,7 +117,7 @@ %span.nav-item-name = _('Messages') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" }) do = link_to admin_broadcast_messages_path do %strong.fly-out-top-item-name = _('Messages') @@ -129,7 +129,7 @@ %span.nav-item-name = _('System Hooks') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" }) do = link_to admin_hooks_path do %strong.fly-out-top-item-name = _('System Hooks') @@ -141,7 +141,7 @@ %span.nav-item-name = _('Applications') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" }) do = link_to admin_applications_path do %strong.fly-out-top-item-name = _('Applications') @@ -154,7 +154,7 @@ = _('Abuse Reports') = gl_badge_tag number_with_delimiter(AbuseReport.count(:all)), variant: :info, size: :sm %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" }) do = link_to admin_abuse_reports_path do %strong.fly-out-top-item-name = _('Abuse Reports') @@ -170,7 +170,7 @@ %span.nav-item-name = _('Kubernetes') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" }) do = link_to admin_clusters_path do %strong.fly-out-top-item-name = _('Kubernetes') @@ -183,7 +183,7 @@ %span.nav-item-name = _('Spam Logs') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" }) do = link_to admin_spam_logs_path do %strong.fly-out-top-item-name = _('Spam Logs') @@ -199,7 +199,7 @@ %span.nav-item-name = _('Deploy Keys') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" }) do = link_to admin_deploy_keys_path do %strong.fly-out-top-item-name = _('Deploy Keys') @@ -213,7 +213,7 @@ %span.nav-item-name = _('Labels') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" }) do = link_to admin_labels_path do %strong.fly-out-top-item-name = _('Labels') @@ -227,7 +227,7 @@ %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } } -# This active_nav_link check is also used in `app/views/layouts/admin.html.haml` - = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" }) do = link_to general_admin_application_settings_path do %strong.fly-out-top-item-name = _('Settings') @@ -273,7 +273,7 @@ = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do %span = _('Network') - = nav_link(controller: :appearances ) do + = nav_link(controller: :appearances) do = link_to admin_application_settings_appearances_path do %span = _('Appearance') diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 0e3327935ca..e1978009114 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -12,7 +12,7 @@ %span.nav-item-name = _('Profile') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" } ) do + = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" }) do = link_to profile_path do %strong.fly-out-top-item-name = _('Profile') @@ -23,7 +23,7 @@ %span.nav-item-name = _('Account') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" }) do = link_to profile_account_path do %strong.fly-out-top-item-name = _('Account') @@ -36,7 +36,7 @@ %span.nav-item-name = _('Applications') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" }) do = link_to applications_profile_path do %strong.fly-out-top-item-name = _('Applications') @@ -47,7 +47,7 @@ %span.nav-item-name = _('Chat') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" }) do = link_to profile_chat_names_path do %strong.fly-out-top-item-name = _('Chat') @@ -59,7 +59,7 @@ %span.nav-item-name = _('Access Tokens') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" }) do = link_to profile_personal_access_tokens_path do %strong.fly-out-top-item-name = _('Access Tokens') @@ -70,7 +70,7 @@ %span.nav-item-name = _('Emails') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" }) do = link_to profile_emails_path do %strong.fly-out-top-item-name = _('Emails') @@ -82,7 +82,7 @@ %span.nav-item-name = _('Password') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" }) do = link_to edit_profile_password_path do %strong.fly-out-top-item-name = _('Password') @@ -93,7 +93,7 @@ %span.nav-item-name = _('Notifications') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" }) do = link_to profile_notifications_path do %strong.fly-out-top-item-name = _('Notifications') @@ -104,7 +104,7 @@ %span.nav-item-name = _('SSH Keys') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" }) do = link_to profile_keys_path do %strong.fly-out-top-item-name = _('SSH Keys') @@ -115,7 +115,7 @@ %span.nav-item-name = _('GPG Keys') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" }) do = link_to profile_gpg_keys_path do %strong.fly-out-top-item-name = _('GPG Keys') @@ -126,7 +126,7 @@ %span.nav-item-name = _('Preferences') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" }) do = link_to profile_preferences_path do %strong.fly-out-top-item-name = _('Preferences') @@ -137,7 +137,7 @@ %span.nav-item-name = _('Active Sessions') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" }) do = link_to profile_active_sessions_path do %strong.fly-out-top-item-name = _('Active Sessions') @@ -148,7 +148,7 @@ %span.nav-item-name = _('Authentication log') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" } ) do + = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" }) do = link_to audit_log_profile_path do %strong.fly-out-top-item-name = _('Authentication Log') diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml index bdf2a1136d3..d6812821966 100644 --- a/app/views/notify/autodevops_disabled_email.html.haml +++ b/app/views/notify/autodevops_disabled_email.html.haml @@ -11,7 +11,7 @@ - link_style = "color: #1b69b6; text-decoration:none;" - pipeline_link = link_to("\##{@pipeline.iid}", pipeline_url(@pipeline), style: link_style).html_safe - project_link = link_to(@project.name, project_url(@project), style: link_style).html_safe - - supported_langs_link = link_to(s_('Notify|currently supported languages'), 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: link_style ).html_safe + - supported_langs_link = link_to(s_('Notify|currently supported languages'), 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: link_style).html_safe - settings_link = link_to(s_('Notify|CI/CD project settings'), project_settings_ci_cd_url(@project), style: link_style).html_safe = s_('Notify|The Auto DevOps pipeline failed for pipeline %{pipeline_link} and has been disabled for %{project_link}. In order to use the Auto DevOps pipeline with your project, please review the %{supported_langs_link}, adjust your project accordingly, and turn on the Auto DevOps pipeline within your %{settings_link}.').html_safe % { pipeline_link: pipeline_link, project_link: project_link, supported_langs_link: supported_langs_link, settings_link: settings_link } diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml index c77a863d1a4..666aa45540e 100644 --- a/app/views/notify/issue_moved_email.html.haml +++ b/app/views/notify/issue_moved_email.html.haml @@ -2,6 +2,6 @@ = s_('Notify|Issue was moved to another project.') - if @can_access_project %p - = sprintf(s_('Notify|New issue: %{project_issue_url}'), { project_issue_url: link_to(@new_issue.title, project_issue_url(@new_project, @new_issue)) } ).html_safe + = sprintf(s_('Notify|New issue: %{project_issue_url}'), { project_issue_url: link_to(@new_issue.title, project_issue_url(@new_project, @new_issue)) }).html_safe - else = s_("Notify|You don't have access to the project.") diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index be9140b23ff..8ef2819fb9b 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -103,7 +103,7 @@ - supported_characters = %w(" ' ` ( [ { < * _).map { |char| "<code>#{char}</code>" }.join(', ') = f.gitlab_ui_checkbox_component :markdown_surround_selection, s_('Preferences|Surround text selection when typing quotes or brackets'), - help_text: sprintf(s_( "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."), { supported_characters: supported_characters }).html_safe + help_text: sprintf(s_("Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."), { supported_characters: supported_characters }).html_safe .form-group = f.gitlab_ui_checkbox_component :markdown_automatic_lists, s_('Preferences|Automatically add new list items'), diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index 249c474587c..4fe68c1ce1a 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -4,12 +4,12 @@ - toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type' = dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector', dropdown_class: 'dropdown-menu-selectable', data: { qa_selector: 'template_type_dropdown' } }) .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } }) .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } }) .metrics-dashboard-selector.js-metrics-dashboard-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } }) #gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } }) .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } }) diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 11984a9d6f6..8ff6d348d95 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -34,7 +34,7 @@ - if load_diff_files_async - url = url_for(safe_params.merge(action: 'diff_files')) .js-diffs-batch{ data: { diff_files_path: url } } - = gl_loading_icon( size: "md", css_class: "gl-mt-4" ) + = gl_loading_icon(size: "md", css_class: "gl-mt-4") - else = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context } diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml index 93cb5ddd7e2..3cc419716e5 100644 --- a/app/views/projects/issues/service_desk.html.haml +++ b/app/views/projects/issues/service_desk.html.haml @@ -1,7 +1,7 @@ - @can_bulk_update = false - page_title _("Service Desk") -- add_page_specific_style 'page_bundles/issues_list' +- add_page_specific_style 'page_bundles/issuable_list' - content_for :breadcrumbs_extra do = render "projects/issues/service_desk/nav_btns", show_export_button: false, show_rss_button: false diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 06123aea960..70bb97b7625 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,6 +1,6 @@ - breadcrumb_title _("Graph") - page_title _("Graph"), @ref -- network_path = Feature.enabled?(:use_ref_type_parameter) ? project_network_path(@project, @id, ref_type: @ref_type ) : project_network_path(@project, @id) +- network_path = Feature.enabled?(:use_ref_type_parameter) ? project_network_path(@project, @id, ref_type: @ref_type) : project_network_path(@project, @id) = render "head" .gl-mt-5 .project-network.gl-border-1.gl-border-solid.gl-border-gray-300 diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index 16312da1353..32e67fdadb8 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -12,7 +12,7 @@ - if verification_enabled - tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success'] .domain-status.ci-status-icon.has-tooltip{ class: "gl-mr-5 ci-status-icon-#{status}", title: tooltip } - = sprite_icon("status_#{status}" ) + = sprite_icon("status_#{status}") .domain-name = external_link(domain.url, domain.url) - if domain.certificate diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 9dc12a2ca41..0de31f59033 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -6,7 +6,7 @@ %td.branch-name-cell.gl-text-truncate{ role: 'cell', data: { label: s_("PipelineSchedules|Target") } } %div - if pipeline_schedule.for_tag? - = sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!' ) + = sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!') - else = sprite_icon('fork', size: 12, css_class: 'gl-vertical-align-middle!') - if pipeline_schedule.ref.present? diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 59b7b6ca334..910aab6da72 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -1,6 +1,6 @@ - page_title _("Container Registry") - @content_class = "limit-container-width" unless fluid_layout -- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil} ) +- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil}) %section #js-container-registry{ data: { endpoint: project_container_registry_index_path(@project), diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index ed06c90efa8..f48404c7407 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -2,7 +2,7 @@ - default_ref = params[:ref] || @project.default_branch - if @error - = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true ) do |c| + = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true) do |c| = c.body do = @error diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 1645c2695b5..8a626f1620b 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -32,17 +32,17 @@ - if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group) %li = render Pajamas::ButtonComponent.new(category: :tertiary, - button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } } ) do + button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } }) do = _('Promote to group label') %li %span = render Pajamas::ButtonComponent.new(category: :tertiary, - button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } } ) do + button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }) do = _('Delete') - if current_user %li.gl-display-inline-block.label-subscription.js-label-subscription.gl-ml-3 - if label.can_subscribe_to_label_in_different_levels? - = render Pajamas::ButtonComponent.new(button_options: { class: "js-unsubscribe-button #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title } ) do + = render Pajamas::ButtonComponent.new(button_options: { class: "js-unsubscribe-button #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do = _('Unsubscribe') .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) } = render Pajamas::ButtonComponent.new(button_options: { class: 'gl-w-full', data: { toggle: 'dropdown' } }) do @@ -51,11 +51,11 @@ .dropdown-menu.dropdown-open-left %ul %li - = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } } ) do + = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }) do = _('Subscribe at project level') %li - = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } } ) do + = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }) do = _('Subscribe at group level') - else - = render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title } ) do + = render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do = label_subscription_toggle_button_text(label, @project) diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index ef41dc9bb79..0053f2fe444 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,6 +1,6 @@ - count_badge_classes = 'gl-display-none gl-sm-display-inline-flex' -= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } } ) do += gl_tabs_nav({class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } }) do = gl_tab_link_to milestones_filter_path(state: 'opened'), { item_active: params[:state].blank? || params[:state] == 'opened' } do = _('Open') = gl_tab_counter_badge counts[:opened], { class: count_badge_classes } diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 8e4b8d6d428..8f2b9fc06e3 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,6 +1,6 @@ - count_badge_classes = 'gl-display-none gl-sm-display-inline-flex' -= gl_tabs_nav( {class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-border-b-0', data: { testid: 'jobs-tabs' } } ) do += gl_tabs_nav({class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-border-b-0', data: { testid: 'jobs-tabs' } }) do = gl_tab_link_to build_path_proc.call(nil), { item_active: scope.nil? } do = _('All') = gl_tab_counter_badge(limited_counter_with_delimiter(all_builds), { class: count_badge_classes }) diff --git a/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml b/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml index 7604445abb2..9fe1400e877 100644 --- a/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml +++ b/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml @@ -1,3 +1,4 @@ - return unless show_security_patch_upgrade_alert? #js-security-patch-upgrade-alert{ data: { "current_version": Gitlab.version_info } } +#js-security-patch-upgrade-alert-modal{ data: { "current_version": Gitlab.version_info, "version": gitlab_version_check.to_json } } diff --git a/app/views/shared/integrations/prometheus/_metrics.html.haml b/app/views/shared/integrations/prometheus/_metrics.html.haml index 8ee0ddfa1b1..c74dbfd8b15 100644 --- a/app/views/shared/integrations/prometheus/_metrics.html.haml +++ b/app/views/shared/integrations/prometheus/_metrics.html.haml @@ -25,8 +25,8 @@ .card.hidden.js-panel-missing-env-vars .card-header - = sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right' ) - = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden' ) + = sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right') + = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden') = s_('PrometheusService|Missing environment variable') = gl_badge_tag 0, nil, class: 'js-env-var-count' .card-body.hidden diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 327461b25fd..39a123f4775 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -101,7 +101,7 @@ .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } = sprite_icon('long-arrow') .dropdown.sidebar-move-issue-dropdown.hide-collapsed - = render Pajamas::ButtonComponent.new(block: true, button_options: { class: 'js-sidebar-dropdown-toggle js-move-issue', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } } ) do + = render Pajamas::ButtonComponent.new(block: true, button_options: { class: 'js-sidebar-dropdown-toggle js-move-issue', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } }) do = _('Move issue') .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height = dropdown_title(_('Move issue')) diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index 51f49c7ca8e..0f6ef33d532 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -4,8 +4,8 @@ - no_issuable_templates = issuable_templates(ref_project, issuable.to_ability_name).empty? - toggle_wip_link_start = '<a href="" class="js-toggle-wip">' - toggle_wip_link_end = '</a>' -- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request draft from merging before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe } ).html_safe -- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.' ) % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe } ).html_safe +- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request draft from merging before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe }).html_safe +- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe }).html_safe %div{ data: { testid: 'issue-title-input-field' } } = form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true, diff --git a/app/views/shared/nav/_sidebar_submenu.html.haml b/app/views/shared/nav/_sidebar_submenu.html.haml index 344dafe7c0f..33b48470020 100644 --- a/app/views/shared/nav/_sidebar_submenu.html.haml +++ b/app/views/shared/nav/_sidebar_submenu.html.haml @@ -1,5 +1,5 @@ %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) } - = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do + = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' }) do %span.fly-out-top-item-container %strong.fly-out-top-item-name = sidebar_menu.title diff --git a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml index d10196a83cc..0244f9e2158 100644 --- a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml +++ b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml @@ -9,7 +9,7 @@ %td.merge_access_levels-container = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level - = dropdown_tag( (merge_access_levels.first&.humanize || 'Select') , + = dropdown_tag((merge_access_levels.first&.humanize || 'Select') , options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header', data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }}) - if user_merge_access_levels.any? @@ -22,7 +22,7 @@ %td.push_access_levels-container = hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level - = dropdown_tag( (push_access_levels.first&.humanize || 'Select') , + = dropdown_tag((push_access_levels.first&.humanize || 'Select') , options: { toggle_class: "js-allowed-to-push js-multiselect", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header', data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }}) - if user_push_access_levels.any? diff --git a/config/feature_flags/development/projects_preloader_fix.yml b/config/feature_flags/development/projects_preloader_fix.yml index 1ad578f11a4..58c56257028 100644 --- a/config/feature_flags/development/projects_preloader_fix.yml +++ b/config/feature_flags/development/projects_preloader_fix.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378858 milestone: '15.6' type: development group: group::workspace -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/route_hll_to_snowplow_phase4.yml b/config/feature_flags/development/route_hll_to_snowplow_phase4.yml new file mode 100644 index 00000000000..a4109b15533 --- /dev/null +++ b/config/feature_flags/development/route_hll_to_snowplow_phase4.yml @@ -0,0 +1,8 @@ +--- +name: route_hll_to_snowplow_phase4 +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103528" +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366767 +milestone: '15.7' +type: development +group: group::product intelligence +default_enabled: false diff --git a/doc/api/deployments.md b/doc/api/deployments.md index daf2b635855..688806e9b22 100644 --- a/doc/api/deployments.md +++ b/doc/api/deployments.md @@ -15,20 +15,25 @@ Get a list of deployments in a project. GET /projects/:id/deployments ``` -| Attribute | Type | Required | Description | -|------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | -| `order_by` | string | no | Return deployments ordered by either one of `id`, `iid`, `created_at`, `updated_at` or `ref` fields. Default is `id`. | -| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc`. | -| `updated_after` | datetime | no | Return deployments updated after the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | -| `updated_before` | datetime | no | Return deployments updated before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | -| `environment` | string | no | The [name of the environment](../ci/environments/index.md) to filter deployments by. | -| `status` | string | no | The status to filter deployments by. One of `created`, `running`, `success`, `failed`, `canceled`, or `blocked`. +| Attribute | Type | Required | Description | +|-------------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `order_by` | string | no | Return deployments ordered by either one of `id`, `iid`, `created_at`, `updated_at`, `finished_at` or `ref` fields. Default is `id`. | +| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc`. | +| `updated_after` | datetime | no | Return deployments updated after the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `updated_before` | datetime | no | Return deployments updated before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `finished_after` | datetime | no | Return deployments finished after the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `finished_before` | datetime | no | Return deployments finished before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). | +| `environment` | string | no | The [name of the environment](../ci/environments/index.md) to filter deployments by. | +| `status` | string | no | The status to filter deployments by. One of `created`, `running`, `success`, `failed`, `canceled`, or `blocked`. ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments" ``` +NOTE: +When using `finished_before` or `finished_after`, you should specify the `order_by` to be `finished_at` and `status` should be `success`. + Example response: ```json diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c9ffbc4943d..bd0bd2e6288 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -10895,6 +10895,20 @@ CI/CD config variables. | <a id="ciconfigvariablevalue"></a>`value` | [`String`](#string) | Value of the variable. | | <a id="ciconfigvariablevalueoptions"></a>`valueOptions` | [`[String!]`](#string) | Value options for the variable. | +### `CiFreezePeriod` + +Represents a deployment freeze window of a project. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="cifreezeperiodendcron"></a>`endCron` | [`String!`](#string) | End of the freeze period in cron format. | +| <a id="cifreezeperiodendtime"></a>`endTime` | [`Time`](#time) | Timestamp (UTC) of when the current/next active period ends. | +| <a id="cifreezeperiodstartcron"></a>`startCron` | [`String!`](#string) | Start of the freeze period in cron format. | +| <a id="cifreezeperiodstarttime"></a>`startTime` | [`Time`](#time) | Timestamp (UTC) of when the current/next active period starts. | +| <a id="cifreezeperiodstatus"></a>`status` | [`CiFreezePeriodStatus!`](#cifreezeperiodstatus) | Freeze period status. | + ### `CiGroup` #### Fields @@ -12422,6 +12436,7 @@ Describes where code is deployed for a project. | <a id="environmentautodeleteat"></a>`autoDeleteAt` | [`Time`](#time) | When the environment is going to be deleted automatically. | | <a id="environmentautostopat"></a>`autoStopAt` | [`Time`](#time) | When the environment is going to be stopped automatically. | | <a id="environmentcreatedat"></a>`createdAt` | [`Time`](#time) | When the environment was created. | +| <a id="environmentdeployfreezes"></a>`deployFreezes` | [`[CiFreezePeriod!]`](#cifreezeperiod) | Deployment freeze periods of the environment. | | <a id="environmentenvironmenttype"></a>`environmentType` | [`String`](#string) | Folder name of the environment. | | <a id="environmentexternalurl"></a>`externalUrl` | [`String`](#string) | External URL of the environment. | | <a id="environmentid"></a>`id` | [`ID!`](#id) | ID of the environment. | @@ -21113,6 +21128,15 @@ Values for YAML processor result. | <a id="ciconfigstatusinvalid"></a>`INVALID` | Configuration file is not valid. | | <a id="ciconfigstatusvalid"></a>`VALID` | Configuration file is valid. | +### `CiFreezePeriodStatus` + +Deploy freeze period status. + +| Value | Description | +| ----- | ----------- | +| <a id="cifreezeperiodstatusactive"></a>`ACTIVE` | Freeze period is active. | +| <a id="cifreezeperiodstatusinactive"></a>`INACTIVE` | Freeze period is inactive. | + ### `CiJobKind` | Value | Description | diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md index 37054c61a85..101fdbf4c2d 100644 --- a/doc/architecture/blueprints/work_items/index.md +++ b/doc/architecture/blueprints/work_items/index.md @@ -61,7 +61,7 @@ All Work Item types share the same pool of predefined widgets and are customized | description | | | hierarchy | | | [iteration](https://gitlab.com/gitlab-org/gitlab/-/issues/367456) | | -| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | work_items_mvc | +| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | | | labels | | | start and due date | | | status\* | | diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 90f93e1db3f..3e55b334992 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -767,6 +767,28 @@ in your merge request fails. ### Text for links +Follow these guidelines for link text. + +#### Standard text + +As much as possible, use text that follows one of these patterns: + +- `For more information, see [LINK TEXT](LINK)`. +- `To [DO THIS THING], see [LINK TEXT](LINK)` + +For example: + +- `For more information, see [merge requests](../../../user/project/merge_requests/index.md).` +- `To create a review app, see [review apps](../../../ci/review_apps/index.md).` + +You can expand on this text by using phrases like +`For more information about this feature, see...` + +Do not to use alternate phrases, like `Learn more about...` or +`To read more...`. + +#### Descriptive text rather than `here` + Use descriptive text for links, rather than words like `here` or `this page.` For example, instead of: @@ -778,6 +800,14 @@ Use: - `For more information, see [merge requests](LINK)`. +#### Links to issues + +When linking to an issue, include the issue number in the link. For example: + +- `For more information, see [issue 12345](LINK).` + +Do not use the pound sign (`issue #12345`). + ### Links to external documentation When possible, avoid links to external documentation. These links can easily become outdated, and are difficult to maintain. diff --git a/doc/development/documentation/versions.md b/doc/development/documentation/versions.md index 14a58452598..e7227a71120 100644 --- a/doc/development/documentation/versions.md +++ b/doc/development/documentation/versions.md @@ -203,8 +203,8 @@ We cannot guarantee future feature work, and promises like these can raise legal issues. Instead, say that an issue exists. For example: -- Support for improvements is proposed in issue `[issue-number](LINK-TO-ISSUE)`. -- You cannot do this thing, but issue `[issue-number](LINK-TO-ISSUE)` proposes to change this behavior. +- Support for improvements is proposed in `[issue [LINK](LINK-TO-ISSUE)`. +- You cannot do this thing, but `[issue [LINK](LINK-TO-ISSUE)` proposes to change this behavior. You can say that we plan to remove a feature. diff --git a/doc/user/tasks.md b/doc/user/tasks.md index 9226f8d15c9..def485b2c04 100644 --- a/doc/user/tasks.md +++ b/doc/user/tasks.md @@ -209,10 +209,7 @@ To set a start date: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) in GitLab 15.5 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) to feature flag named `work_items_mvc` in GitLab 15.7. Disabled by default. - -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc`. -On GitLab.com, this feature is not available. The feature is not ready for production use. +> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) in GitLab 15.7. You can add a task to a [milestone](project/milestones/index.md). You can see the milestone title when you view a task. diff --git a/lib/atlassian/jira_connect/jwt/asymmetric.rb b/lib/atlassian/jira_connect/jwt/asymmetric.rb index 573a8022752..7c1cf1cabb6 100644 --- a/lib/atlassian/jira_connect/jwt/asymmetric.rb +++ b/lib/atlassian/jira_connect/jwt/asymmetric.rb @@ -77,11 +77,7 @@ module Atlassian end def public_key_cdn_url - if public_key_cdn_url_setting.blank? || Feature.disabled?(:jira_connect_oauth_self_managed) - return DEFAULT_PUBLIC_KEY_CDN_URL - end - - public_key_cdn_url_setting + public_key_cdn_url_setting.presence || DEFAULT_PUBLIC_KEY_CDN_URL end def public_key_cdn_url_setting diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 55b10c008fb..9b019f16ddd 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -157,7 +157,7 @@ module Backup end def backup_files_realpath - @backup_files_realpath ||= File.join(Gitlab.config.backup.path, File.basename(@app_files_dir) ) + @backup_files_realpath ||= File.join(Gitlab.config.backup.path, File.basename(@app_files_dir)) end end end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 7de6be45349..49b8ab760f3 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -98,7 +98,7 @@ module Gitlab create_labels - issue_type_id = WorkItems::Type.default_issue_type.id + issue_type_id = ::WorkItems::Type.default_issue_type.id client.issues(repo).each do |issue| import_issue(issue, issue_type_id) diff --git a/lib/gitlab/ci/reports/security/finding.rb b/lib/gitlab/ci/reports/security/finding.rb index dd9b9cc6d55..4062132b970 100644 --- a/lib/gitlab/ci/reports/security/finding.rb +++ b/lib/gitlab/ci/reports/security/finding.rb @@ -98,7 +98,7 @@ module Gitlab end def unsafe?(severity_levels, report_types) - severity.to_s.in?(severity_levels) && (report_types.blank? || report_type.to_s.in?(report_types) ) + severity.to_s.in?(severity_levels) && (report_types.blank? || report_type.to_s.in?(report_types)) end def eql?(other) diff --git a/lib/gitlab/ci/runner_instructions.rb b/lib/gitlab/ci/runner_instructions.rb index 01df41a06ce..bcda2fec5ba 100644 --- a/lib/gitlab/ci/runner_instructions.rb +++ b/lib/gitlab/ci/runner_instructions.rb @@ -91,7 +91,7 @@ module Gitlab end def environment - @environment ||= OS[@os.to_sym] || ( raise Gitlab::Ci::RunnerInstructions::ArgumentError, _('Invalid OS') ) + @environment ||= OS[@os.to_sym] || (raise Gitlab::Ci::RunnerInstructions::ArgumentError, _('Invalid OS')) end def validate_params diff --git a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb index 4e38eea963b..fd99062974c 100644 --- a/lib/gitlab/database/partitioning/single_numeric_list_partition.rb +++ b/lib/gitlab/database/partitioning/single_numeric_list_partition.rb @@ -19,7 +19,7 @@ module Gitlab attr_reader :table, :value - def initialize(table, value, partition_name: nil ) + def initialize(table, value, partition_name: nil) @table = table @value = value @partition_name = partition_name diff --git a/lib/gitlab/database/postgres_hll/buckets.rb b/lib/gitlab/database/postgres_hll/buckets.rb index cbc9544d905..3f64eee030e 100644 --- a/lib/gitlab/database/postgres_hll/buckets.rb +++ b/lib/gitlab/database/postgres_hll/buckets.rb @@ -61,7 +61,7 @@ module Gitlab num_uniques = ( ((TOTAL_BUCKETS**2) * (0.7213 / (1 + 1.079 / TOTAL_BUCKETS))) / - (num_zero_buckets + buckets.values.sum { |bucket_hash| 2**(-1 * bucket_hash) } ) + (num_zero_buckets + buckets.values.sum { |bucket_hash| 2**(-1 * bucket_hash) }) ).to_i if num_zero_buckets > 0 && num_uniques < 2.5 * TOTAL_BUCKETS diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 924c28e3db5..b29c75ed467 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -73,7 +73,7 @@ module Gitlab private def filename?(line) - line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b', + line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b', '+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit '--- /tmp/diffy', '+++ /tmp/diffy') end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 6bcf4802fbe..de66ca7305f 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -571,7 +571,7 @@ module Gitlab end def encode_repeated(array) - Google::Protobuf::RepeatedField.new(:bytes, array.map { |s| encode_binary(s) } ) + Google::Protobuf::RepeatedField.new(:bytes, array.map { |s| encode_binary(s) }) end def call_find_commit(revision) diff --git a/lib/gitlab/import_export/decompressed_archive_size_validator.rb b/lib/gitlab/import_export/decompressed_archive_size_validator.rb index aa66fe8a5ae..564008e7a73 100644 --- a/lib/gitlab/import_export/decompressed_archive_size_validator.rb +++ b/lib/gitlab/import_export/decompressed_archive_size_validator.rb @@ -35,7 +35,7 @@ module Gitlab Timeout.timeout(TIMEOUT_LIMIT) do stderr_r, stderr_w = IO.pipe - stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w ) + stdout, wait_threads = Open3.pipeline_r(*command, pgroup: true, err: stderr_w) # When validation is performed on a small archive (e.g. 100 bytes) # `wait_thr` finishes before we can get process group id. Do not diff --git a/lib/gitlab/jira_import/issues_importer.rb b/lib/gitlab/jira_import/issues_importer.rb index 5057317ae01..7b031c26b72 100644 --- a/lib/gitlab/jira_import/issues_importer.rb +++ b/lib/gitlab/jira_import/issues_importer.rb @@ -16,7 +16,7 @@ module Gitlab @start_at = Gitlab::JiraImport.get_issues_next_start_at(project.id) @imported_items_cache_key = JiraImport.already_imported_cache_key(:issues, project.id) @job_waiter = JobWaiter.new - @issue_type_id = WorkItems::Type.default_issue_type.id + @issue_type_id = ::WorkItems::Type.default_issue_type.id end def execute diff --git a/lib/gitlab/memory/watchdog/monitor_state.rb b/lib/gitlab/memory/watchdog/monitor_state.rb index 2562599d2ab..bb083fedf2c 100644 --- a/lib/gitlab/memory/watchdog/monitor_state.rb +++ b/lib/gitlab/memory/watchdog/monitor_state.rb @@ -7,7 +7,7 @@ module Gitlab class Result attr_reader :payload, :monitor_name - def initialize(strikes_exceeded:, threshold_violated:, monitor_name:, payload: ) + def initialize(strikes_exceeded:, threshold_violated:, monitor_name:, payload:) @strikes_exceeded = strikes_exceeded @threshold_violated = threshold_violated @monitor_name = monitor_name.to_s.to_sym diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb index dda28ffdf90..6a5613ddd98 100644 --- a/lib/gitlab/prometheus_client.rb +++ b/lib/gitlab/prometheus_client.rb @@ -150,7 +150,7 @@ module Gitlab end def get(path, args) - Gitlab::HTTP.get(path, { query: args }.merge(http_options) ) + Gitlab::HTTP.get(path, { query: args }.merge(http_options)) rescue *Gitlab::HTTP::HTTP_ERRORS => e raise PrometheusClient::ConnectionError, e.message end diff --git a/lib/gitlab/sidekiq_daemon/memory_killer.rb b/lib/gitlab/sidekiq_daemon/memory_killer.rb index d5227e7a007..c535e103420 100644 --- a/lib/gitlab/sidekiq_daemon/memory_killer.rb +++ b/lib/gitlab/sidekiq_daemon/memory_killer.rb @@ -78,7 +78,7 @@ module Gitlab rescue StandardError => e log_exception(e, __method__) rescue Exception => e # rubocop:disable Lint/RescueException - log_exception(e, __method__ ) + log_exception(e, __method__) raise e end end @@ -188,7 +188,7 @@ module Gitlab def increment_worker_counters(running_jobs, deadline_exceeded) running_jobs.each do |job| - @metrics[:sidekiq_memory_killer_running_jobs].increment( { worker_class: job[:worker_class], deadline_exceeded: deadline_exceeded } ) + @metrics[:sidekiq_memory_killer_running_jobs].increment({ worker_class: job[:worker_class], deadline_exceeded: deadline_exceeded }) end end diff --git a/lib/gitlab/tracking/incident_management.rb b/lib/gitlab/tracking/incident_management.rb index df2a0658b36..a912fdbaeca 100644 --- a/lib/gitlab/tracking/incident_management.rb +++ b/lib/gitlab/tracking/incident_management.rb @@ -17,7 +17,7 @@ module Gitlab details = label ? { label: label, property: v } : {} - ::Gitlab::Tracking.event('IncidentManagement::Settings', "#{prefix}_#{key}", **details ) + ::Gitlab::Tracking.event('IncidentManagement::Settings', "#{prefix}_#{key}", **details) end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 7360585df43..8b016a09889 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -11,7 +11,7 @@ module Gitlab included do scope :public_only, -> { where(visibility_level: PUBLIC) } - scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } + scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL]) } scope :private_only, -> { where(visibility_level: PRIVATE) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } diff --git a/lib/gitlab/work_items/work_item_hierarchy.rb b/lib/gitlab/work_items/work_item_hierarchy.rb new file mode 100644 index 00000000000..e71bf2bce6a --- /dev/null +++ b/lib/gitlab/work_items/work_item_hierarchy.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module WorkItems + class WorkItemHierarchy < ObjectHierarchy + extend ::Gitlab::Utils::Override + + private + + def middle_table + ::WorkItems::ParentLink.arel_table + end + + def from_tables(cte) + [objects_table, cte.table, middle_table] + end + + override :parent_id_column + def parent_id_column(cte) + middle_table[:work_item_parent_id] + end + + override :ancestor_conditions + def ancestor_conditions(cte) + conditions = middle_table[:work_item_parent_id].eq(objects_table[:id]).and( + middle_table[:work_item_id].eq(cte.table[:id]) + ) + + with_type_filter(conditions, cte) + end + + override :descendant_conditions + def descendant_conditions(cte) + conditions = middle_table[:work_item_id].eq(objects_table[:id]).and( + middle_table[:work_item_parent_id].eq(cte.table[:id]) + ) + + with_type_filter(conditions, cte) + end + + def with_type_filter(conditions, cte) + return conditions unless options[:same_type] + + conditions.and(objects_table[:work_item_type_id].eq(cte.table[:work_item_type_id])) + end + end + end +end diff --git a/lib/security/ci_configuration/sast_build_action.rb b/lib/security/ci_configuration/sast_build_action.rb index 448d4fbeacb..2b1964f7c87 100644 --- a/lib/security/ci_configuration/sast_build_action.rb +++ b/lib/security/ci_configuration/sast_build_action.rb @@ -68,7 +68,7 @@ module Security end def auto_devops_stages - auto_devops_template = YAML.safe_load( Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content ) + auto_devops_template = YAML.safe_load(Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content) auto_devops_template['stages'] end diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake index 1b9ff33415f..34ef4b139c3 100644 --- a/lib/tasks/gitlab/sidekiq.rake +++ b/lib/tasks/gitlab/sidekiq.rake @@ -10,21 +10,21 @@ namespace :gitlab do desc 'GitLab | Sidekiq | Migrate jobs in the scheduled set to new queue names' task schedule: :environment do ::Gitlab::SidekiqMigrateJobs - .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout) ) + .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout)) .migrate_set('schedule') end desc 'GitLab | Sidekiq | Migrate jobs in the retry set to new queue names' task retry: :environment do ::Gitlab::SidekiqMigrateJobs - .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout) ) + .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout)) .migrate_set('retry') end desc 'GitLab | Sidekiq | Migrate jobs in queues outside of routing rules' task queued: :environment do ::Gitlab::SidekiqMigrateJobs - .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout) ) + .new(::Gitlab::SidekiqConfig.worker_queue_mappings, logger: Logger.new($stdout)) .migrate_queues end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b97935d849e..258d61624f2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -45306,12 +45306,21 @@ msgstr "" msgid "Version %{versionNumber} (latest)" msgstr "" +msgid "VersionCheck|%{details}" +msgstr "" + msgid "VersionCheck|Critical security upgrade available" msgstr "" +msgid "VersionCheck|Important notice - Critical security release" +msgstr "" + msgid "VersionCheck|Learn more about this critical security release." msgstr "" +msgid "VersionCheck|Remind me again in 3 days" +msgstr "" + msgid "VersionCheck|Up to date" msgstr "" @@ -45324,6 +45333,12 @@ msgstr "" msgid "VersionCheck|Upgrade now" msgstr "" +msgid "VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation immediately." +msgstr "" + +msgid "VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation to one of the following versions immediately: %{latestStableVersions}." +msgstr "" + msgid "VersionCheck|You are currently on version %{currentVersion}. We strongly recommend upgrading your GitLab installation. %{link}" msgstr "" @@ -49082,6 +49097,9 @@ msgstr "" msgid "is already linked to this vulnerability" msgstr "" +msgid "is already present in ancestors" +msgstr "" + msgid "is an invalid IP address range" msgstr "" @@ -49124,6 +49142,9 @@ msgstr "" msgid "is not allowed to add this type of parent" msgstr "" +msgid "is not allowed to point to itself" +msgstr "" + msgid "is not allowed. Please use your regular email address." msgstr "" @@ -49836,6 +49857,9 @@ msgstr "" msgid "reCAPTCHA site key" msgstr "" +msgid "reached maximum depth" +msgstr "" + msgid "recent activity" msgstr "" diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js new file mode 100644 index 00000000000..f1ed32a5f79 --- /dev/null +++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js @@ -0,0 +1,202 @@ +import { GlModal, GlLink, GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import { sprintf } from '~/locale'; +import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue'; +import * as utils from '~/gitlab_version_check/utils'; +import { + UPGRADE_DOCS_URL, + ABOUT_RELEASES_PAGE, + TRACKING_ACTIONS, + TRACKING_LABELS, +} from '~/gitlab_version_check/constants'; + +describe('SecurityPatchUpgradeAlertModal', () => { + let wrapper; + let trackingSpy; + + const defaultProps = { + currentVersion: '11.1.1', + }; + + const createComponent = (props = {}) => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + + wrapper = shallowMountExtended(SecurityPatchUpgradeAlertModal, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlModal, + GlSprintf, + }, + }); + }; + + afterEach(() => { + unmockTracking(); + }); + + const expectDispatchedTracking = (action, label) => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, action, { + label, + property: defaultProps.currentVersion, + }); + }; + + const findGlModal = () => wrapper.findComponent(GlModal); + const findGlModalTitle = () => wrapper.findByTestId('alert-modal-title'); + const findGlModalBody = () => wrapper.findByTestId('alert-modal-body'); + const findGlModalDetails = () => wrapper.findByTestId('alert-modal-details'); + const findGlLink = () => wrapper.findComponent(GlLink); + const findGlRemindButton = () => wrapper.findByTestId('alert-modal-remind-button'); + const findGlUpgradeButton = () => wrapper.findByTestId('alert-modal-upgrade-button'); + + describe('template defaults', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders visible critical security alert modal', () => { + expect(findGlModal().props('visible')).toBe(true); + }); + + it('renders the modal title correctly', () => { + expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle); + }); + + it('renders modal body without suggested versions', () => { + expect(findGlModalBody().text()).toBe( + sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, { + currentVersion: defaultProps.currentVersion, + }), + ); + }); + + it('does not render modal details', () => { + expect(findGlModalDetails().exists()).toBe(false); + }); + + it(`tracks render ${TRACKING_LABELS.MODAL} correctly`, () => { + expectDispatchedTracking(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL); + }); + + it(`tracks click ${TRACKING_LABELS.DISMISS} when close button clicked`, async () => { + await findGlModal().vm.$emit('close'); + + expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS); + }); + + describe('Learn more link', () => { + it('renders with correct text and link', () => { + expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore); + expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE); + }); + + it(`tracks click ${TRACKING_LABELS.LEARN_MORE_LINK} when clicked`, async () => { + await findGlLink().vm.$emit('click'); + + expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK); + }); + }); + + describe('Remind me button', () => { + beforeEach(() => { + wrapper.vm.$refs.alertModal.hide = jest.fn(); + }); + + it('renders with correct text', () => { + expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText); + }); + + it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => { + await findGlRemindButton().vm.$emit('click'); + + expectDispatchedTracking(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN); + }); + + it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => { + jest.spyOn(utils, 'setHideAlertModalCookie'); + await findGlRemindButton().vm.$emit('click'); + + expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion); + }); + + it('hides the modal', async () => { + await findGlRemindButton().vm.$emit('click'); + + expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled(); + }); + }); + + describe('Upgrade button', () => { + it('renders with correct text and link', () => { + expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText); + expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL); + }); + + it(`tracks click ${TRACKING_LABELS.UPGRADE_BTN_LINK} when clicked`, async () => { + await findGlUpgradeButton().vm.$emit('click'); + + expectDispatchedTracking(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK); + }); + + it('calls setHideAlertModalCookie with the currentVersion when clicked', async () => { + jest.spyOn(utils, 'setHideAlertModalCookie'); + await findGlUpgradeButton().vm.$emit('click'); + + expect(utils.setHideAlertModalCookie).toHaveBeenCalledWith(defaultProps.currentVersion); + }); + }); + }); + + describe('template with latestStableVersions', () => { + const latestStableVersions = ['88.8.3', '89.9.9', '90.0.0']; + + beforeEach(() => { + createComponent({ latestStableVersions }); + }); + + it('renders modal body with suggested versions', () => { + expect(findGlModalBody().text()).toBe( + sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, { + currentVersion: defaultProps.currentVersion, + latestStableVersions: latestStableVersions.join(', '), + }), + ); + }); + }); + + describe('template with details', () => { + const details = 'This is some details about the upgrade'; + + beforeEach(() => { + createComponent({ details }); + }); + + it('renders modal details', () => { + expect(findGlModalDetails().text()).toBe( + sprintf(wrapper.vm.$options.i18n.modalDetails, { details }), + ); + }); + }); + + describe('when modal is hidden by cookie', () => { + beforeEach(() => { + jest.spyOn(utils, 'getHideAlertModalCookie').mockReturnValue(true); + createComponent(); + }); + + it('renders modal with visibility false', () => { + expect(findGlModal().props('visible')).toBe(false); + }); + + it(`does not track render ${TRACKING_LABELS.MODAL} correctly`, () => { + expect(trackingSpy).not.toHaveBeenCalledWith(undefined, TRACKING_ACTIONS.RENDER, { + label: TRACKING_LABELS.MODAL, + property: defaultProps.currentVersion, + }); + }); + }); +}); diff --git a/spec/frontend/gitlab_version_check/index_spec.js b/spec/frontend/gitlab_version_check/index_spec.js index 9e4d50f2cfb..92bc103cede 100644 --- a/spec/frontend/gitlab_version_check/index_spec.js +++ b/spec/frontend/gitlab_version_check/index_spec.js @@ -9,15 +9,19 @@ import { VERSION_BADGE_TEXT, SECURITY_PATCH_FIXTURE, SECURITY_PATCH_FINDER, - SECURITY_BATCH_TEXT, + SECURITY_PATCH_TEXT, + SECURITY_MODAL_FIXTURE, + SECURITY_MODAL_FINDER, + SECURITY_MODAL_TEXT, } from './mock_data'; describe('initGitlabVersionCheck', () => { - let vueApps; + let wrapper; const createApp = (fixture) => { setHTMLFixture(fixture); - vueApps = initGitlabVersionCheck(); + initGitlabVersionCheck(); + wrapper = createWrapper(document.body); }; afterEach(() => { @@ -25,23 +29,22 @@ describe('initGitlabVersionCheck', () => { }); describe.each` - description | fixture | finders | componentTexts - ${'with no version check elements'} | ${'<div></div>'} | ${[]} | ${[]} - ${'with version check badge el but no prop data'} | ${VERSION_CHECK_BADGE_NO_PROP_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]} - ${'with version check badge el but no severity data'} | ${VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]} - ${'with version check badge el and version data'} | ${VERSION_CHECK_BADGE_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[VERSION_BADGE_TEXT]} - ${'with security patch el'} | ${SECURITY_PATCH_FIXTURE} | ${[SECURITY_PATCH_FINDER]} | ${[SECURITY_BATCH_TEXT]} - ${'with security patch and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_BATCH_TEXT, VERSION_BADGE_TEXT]} + description | fixture | finders | componentTexts + ${'with no version check elements'} | ${'<div></div>'} | ${[]} | ${[]} + ${'with version check badge el but no prop data'} | ${VERSION_CHECK_BADGE_NO_PROP_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]} + ${'with version check badge el but no severity data'} | ${VERSION_CHECK_BADGE_NO_SEVERITY_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[undefined]} + ${'with version check badge el and version data'} | ${VERSION_CHECK_BADGE_FIXTURE} | ${[VERSION_CHECK_BADGE_FINDER]} | ${[VERSION_BADGE_TEXT]} + ${'with security patch el'} | ${SECURITY_PATCH_FIXTURE} | ${[SECURITY_PATCH_FINDER]} | ${[SECURITY_PATCH_TEXT]} + ${'with security patch and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, VERSION_BADGE_TEXT]} + ${'with security modal el'} | ${SECURITY_MODAL_FIXTURE} | ${[SECURITY_MODAL_FINDER]} | ${[SECURITY_MODAL_TEXT]} + ${'with security modal, security patch, and version badge els'} | ${`${SECURITY_PATCH_FIXTURE}${SECURITY_MODAL_FIXTURE}${VERSION_CHECK_BADGE_FIXTURE}`} | ${[SECURITY_PATCH_FINDER, SECURITY_MODAL_FINDER, VERSION_CHECK_BADGE_FINDER]} | ${[SECURITY_PATCH_TEXT, SECURITY_MODAL_TEXT, VERSION_BADGE_TEXT]} `('$description', ({ fixture, finders, componentTexts }) => { beforeEach(() => { createApp(fixture); }); it(`correctly renders the Version Check Components`, () => { - const vueAppInstances = vueApps.map((v) => v && createWrapper(v)); - const renderedComponentTexts = vueAppInstances.map((v, index) => - v?.find(finders[index]).text(), - ); + const renderedComponentTexts = finders.map((f) => wrapper.find(f)?.element?.innerText.trim()); expect(renderedComponentTexts).toStrictEqual(componentTexts); }); diff --git a/spec/frontend/gitlab_version_check/mock_data.js b/spec/frontend/gitlab_version_check/mock_data.js index 6f2107f4d3c..707d45550eb 100644 --- a/spec/frontend/gitlab_version_check/mock_data.js +++ b/spec/frontend/gitlab_version_check/mock_data.js @@ -13,4 +13,10 @@ export const SECURITY_PATCH_FIXTURE = `<div id="js-security-patch-upgrade-alert" export const SECURITY_PATCH_FINDER = 'h2'; -export const SECURITY_BATCH_TEXT = 'Critical security upgrade available'; +export const SECURITY_PATCH_TEXT = 'Critical security upgrade available'; + +export const SECURITY_MODAL_FIXTURE = `<div id="js-security-patch-upgrade-alert-modal" data-current-version="15.1" data-version='{ "details": "test details", "latest-stable-versions": "[]" }'></div>`; + +export const SECURITY_MODAL_FINDER = '[data-testid="alert-modal-title"]'; + +export const SECURITY_MODAL_TEXT = 'Important notice - Critical security release'; diff --git a/spec/frontend/gitlab_version_check/utils_spec.js b/spec/frontend/gitlab_version_check/utils_spec.js new file mode 100644 index 00000000000..6126d88dfec --- /dev/null +++ b/spec/frontend/gitlab_version_check/utils_spec.js @@ -0,0 +1,35 @@ +import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils'; +import { getHideAlertModalCookie, setHideAlertModalCookie } from '~/gitlab_version_check/utils'; +import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from '~/gitlab_version_check/constants'; + +jest.mock('~/lib/utils/common_utils', () => ({ + parseBoolean: jest.fn().mockReturnValue(true), + getCookie: jest.fn().mockReturnValue('true'), + setCookie: jest.fn(), +})); + +describe('GitLab Version Check Utils', () => { + describe('setHideAlertModalCookie', () => { + it('properly generates a key based on the currentVersion and sets Cookie to `true`', () => { + const currentVersion = '99.9.9'; + + setHideAlertModalCookie(currentVersion); + + expect(setCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`, true, { + expires: COOKIE_EXPIRATION, + }); + }); + }); + + describe('getHideAlertModalCookie', () => { + it('properly generates a key based on the currentVersion, fetches said Cooke, and parsesBoolean it', () => { + const currentVersion = '99.9.9'; + + const res = getHideAlertModalCookie(currentVersion); + + expect(getCookie).toHaveBeenCalledWith(`${currentVersion}${COOKIE_SUFFIX}`); + expect(parseBoolean).toHaveBeenCalledWith('true'); + expect(res).toBe(true); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 2a347892973..30475b36561 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -575,7 +575,7 @@ describe('WorkItemDetail component', () => { `('$description', async ({ milestoneWidgetPresent, exists }) => { const response = workItemResponseFactory({ milestoneWidgetPresent }); const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler, workItemsMvcEnabled: true }); + createComponent({ handler }); await waitForPromises(); expect(findWorkItemMilestone().exists()).toBe(exists); diff --git a/spec/graphql/types/ci/freeze_period_status_enum_spec.rb b/spec/graphql/types/ci/freeze_period_status_enum_spec.rb new file mode 100644 index 00000000000..2d9c7071392 --- /dev/null +++ b/spec/graphql/types/ci/freeze_period_status_enum_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiFreezePeriodStatus'], feature_category: :release_orchestration do + it 'exposes all freeze period statuses' do + expect(described_class.values.keys).to contain_exactly(*%w[ACTIVE INACTIVE]) + end +end diff --git a/spec/graphql/types/ci/freeze_period_type_spec.rb b/spec/graphql/types/ci/freeze_period_type_spec.rb new file mode 100644 index 00000000000..ff61eadae60 --- /dev/null +++ b/spec/graphql/types/ci/freeze_period_type_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiFreezePeriod'], feature_category: :release_orchestration do + specify { expect(described_class.graphql_name).to eq('CiFreezePeriod') } + + it 'has the expected fields' do + expected_fields = %w[ + status start_cron end_cron start_time end_time + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + specify { expect(described_class).to require_graphql_authorizations(:read_freeze_period) } +end diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb index a891a5c285b..4471735876a 100644 --- a/spec/graphql/types/environment_type_spec.rb +++ b/spec/graphql/types/environment_type_spec.rb @@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['Environment'] do it 'includes the expected fields' do expected_fields = %w[ name id state metrics_dashboard latest_opened_most_severe_alert path external_url deployments - slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment + slug createdAt updatedAt autoStopAt autoDeleteAt tier environmentType lastDeployment deployFreezes ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb index 86d672067a3..0ffa04e851b 100644 --- a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb +++ b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do let(:jwt) { JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers) } let(:public_key) { private_key.public_key } let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' } + let(:jira_connect_proxy_url_setting) { nil } let(:install_keys_url) { "#{stub_asymmetric_jwt_cdn}/#{public_key_id}" } let(:qsh) do Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/installed', 'POST', 'https://gitlab.test') @@ -25,6 +26,8 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do before do stub_request(:get, install_keys_url) .to_return(body: public_key.to_s, status: 200) + + stub_application_setting(jira_connect_proxy_url: jira_connect_proxy_url_setting) end it 'returns true when verified with public key from CDN' do @@ -89,10 +92,7 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do context 'with jira_connect_proxy_url setting' do let(:stub_asymmetric_jwt_cdn) { 'https://example.com/-/jira_connect/public_keys' } - - before do - stub_application_setting(jira_connect_proxy_url: 'https://example.com') - end + let(:jira_connect_proxy_url_setting) { 'https://example.com' } it 'requests the settings CDN' do expect(JWT).to receive(:decode).twice.and_call_original @@ -101,22 +101,6 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do expect(WebMock).to have_requested(:get, "https://example.com/-/jira_connect/public_keys/#{public_key_id}") end - - context 'when jira_connect_oauth_self_managed disabled' do - let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' } - - before do - stub_feature_flags(jira_connect_oauth_self_managed: false) - end - - it 'requests the default CDN' do - expect(JWT).to receive(:decode).twice.and_call_original - - expect(asymmetric_jwt).to be_valid - - expect(WebMock).to have_requested(:get, install_keys_url) - end - end end end diff --git a/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb b/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb new file mode 100644 index 00000000000..b2f298a7d05 --- /dev/null +++ b/spec/lib/gitlab/work_items/work_item_hierarchy_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::WorkItems::WorkItemHierarchy, feature_category: :portfolio_management do + let_it_be(:project) { create(:project) } + let_it_be(:type1) { create(:work_item_type, namespace: project.namespace) } + let_it_be(:type2) { create(:work_item_type, namespace: project.namespace) } + let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) } + let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type2) } + let_it_be(:hierarchy_restriction3) { create(:hierarchy_restriction, parent_type: type2, child_type: type1) } + let_it_be(:item1) { create(:work_item, work_item_type: type1, project: project) } + let_it_be(:item2) { create(:work_item, work_item_type: type2, project: project) } + let_it_be(:item3) { create(:work_item, work_item_type: type2, project: project) } + let_it_be(:item4) { create(:work_item, work_item_type: type1, project: project) } + let_it_be(:ignored1) { create(:work_item, work_item_type: type1, project: project) } + let_it_be(:ignored2) { create(:work_item, work_item_type: type2, project: project) } + let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) } + let_it_be(:link2) { create(:parent_link, work_item_parent: item2, work_item: item3) } + let_it_be(:link3) { create(:parent_link, work_item_parent: item3, work_item: item4) } + + let(:options) { {} } + + describe '#base_and_ancestors' do + subject { described_class.new(::WorkItem.where(id: item3.id), options: options) } + + it 'includes the base and its ancestors' do + relation = subject.base_and_ancestors + + expect(relation).to eq([item3, item2, item1]) + end + + context 'when same_type option is used' do + let(:options) { { same_type: true } } + + it 'includes the base and its ancestors' do + relation = subject.base_and_ancestors + + expect(relation).to eq([item3, item2]) + end + end + + it 'can find ancestors upto a certain level' do + relation = subject.base_and_ancestors(upto: item1) + + expect(relation).to eq([item3, item2]) + end + + describe 'hierarchy_order option' do + let(:relation) do + subject.base_and_ancestors(hierarchy_order: hierarchy_order) + end + + context 'for :asc' do + let(:hierarchy_order) { :asc } + + it 'orders by child to ancestor' do + expect(relation).to eq([item3, item2, item1]) + end + end + + context 'for :desc' do + let(:hierarchy_order) { :desc } + + it 'orders by ancestor to child' do + expect(relation).to eq([item1, item2, item3]) + end + end + end + end + + describe '#base_and_descendants' do + subject { described_class.new(::WorkItem.where(id: item2.id), options: options) } + + it 'includes the base and its descendants' do + relation = subject.base_and_descendants + + expect(relation).to eq([item2, item3, item4]) + end + + context 'when same_type option is used' do + let(:options) { { same_type: true } } + + it 'includes the base and its ancestors' do + relation = subject.base_and_descendants + + expect(relation).to eq([item2, item3]) + end + end + + context 'when with_depth is true' do + let(:relation) do + subject.base_and_descendants(with_depth: true) + end + + it 'includes depth in the results' do + object_depths = { + item2.id => 1, + item3.id => 2, + item4.id => 3 + } + + relation.each do |object| + expect(object.depth).to eq(object_depths[object.id]) + end + end + end + end +end diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb index b9bf1657e28..d8add736d6a 100644 --- a/spec/models/ci/freeze_period_spec.rb +++ b/spec/models/ci/freeze_period_spec.rb @@ -2,16 +2,22 @@ require 'spec_helper' -RSpec.describe Ci::FreezePeriod, type: :model do +RSpec.describe Ci::FreezePeriod, feature_category: :release_orchestration, type: :model do + let_it_be(:project) { create(:project) } + + # Freeze period factory is on a weekend, so we travel in time, in and around that. + let(:friday_2300_time) { Time.utc(2020, 4, 10, 23, 0) } + let(:saturday_1200_time) { Time.utc(2020, 4, 11, 12, 0) } + let(:monday_0700_time) { Time.utc(2020, 4, 13, 7, 0) } + let(:tuesday_0800_time) { Time.utc(2020, 4, 14, 8, 0) } + subject { build(:ci_freeze_period) } it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:project) } - let!(:model) { create(:ci_freeze_period, project: parent) } + let!(:model) { create(:ci_freeze_period, project: parent) } end - let(:invalid_cron) { '0 0 0 * *' } - it { is_expected.to belong_to(:project) } it { is_expected.to respond_to(:freeze_start) } @@ -19,37 +25,142 @@ RSpec.describe Ci::FreezePeriod, type: :model do it { is_expected.to respond_to(:cron_timezone) } describe 'cron validations' do + let(:invalid_cron) { '0 0 0 * *' } + it 'allows valid cron patterns' do - freeze_period = build(:ci_freeze_period) + freeze_period = build_stubbed(:ci_freeze_period) expect(freeze_period).to be_valid end it 'does not allow invalid cron patterns on freeze_start' do - freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron) + freeze_period = build_stubbed(:ci_freeze_period, freeze_start: invalid_cron) expect(freeze_period).not_to be_valid end it 'does not allow invalid cron patterns on freeze_end' do - freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron) + freeze_period = build_stubbed(:ci_freeze_period, freeze_end: invalid_cron) expect(freeze_period).not_to be_valid end it 'does not allow an invalid timezone' do - freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid') + freeze_period = build_stubbed(:ci_freeze_period, cron_timezone: 'invalid') expect(freeze_period).not_to be_valid end context 'when cron contains trailing whitespaces' do it 'strips the attribute' do - freeze_period = build(:ci_freeze_period, freeze_start: ' 0 0 * * * ') + freeze_period = build_stubbed(:ci_freeze_period, freeze_start: ' 0 0 * * * ') expect(freeze_period).to be_valid expect(freeze_period.freeze_start).to eq('0 0 * * *') end end end + + shared_examples 'within freeze period' do |time| + it 'is frozen' do + travel_to(time) do + expect(subject).to eq(Ci::FreezePeriod::STATUS_ACTIVE) + end + end + end + + shared_examples 'outside freeze period' do |time| + it 'is not frozen' do + travel_to(time) do + expect(subject).to eq(Ci::FreezePeriod::STATUS_INACTIVE) + end + end + end + + describe '#status' do + subject { freeze_period.status } + + describe 'single freeze period' do + let(:freeze_period) do + build_stubbed(:ci_freeze_period, project: project) + end + + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) + it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) + it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1) + end + + # See https://gitlab.com/gitlab-org/gitlab/-/issues/370472 + context 'when period overlaps with itself' do + let(:freeze_period) do + build_stubbed(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *') + end + + it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0) + it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0) + end + end + + shared_examples 'a freeze period method' do + let(:freeze_period) { build_stubbed(:ci_freeze_period, project: project) } + + it 'returns the correct value' do + travel_to(now) do + expect(freeze_period.send(method)).to eq(expected) + end + end + end + + describe '#active?' do + context 'when freeze period status is active' do + it_behaves_like 'a freeze period method' do + let(:now) { saturday_1200_time } + let(:method) { :active? } + let(:expected) { true } + end + end + + context 'when freeze period status is inactive' do + it_behaves_like 'a freeze period method' do + let(:now) { tuesday_0800_time } + let(:method) { :active? } + let(:expected) { false } + end + end + end + + describe '#time_start' do + it_behaves_like 'a freeze period method' do + let(:now) { monday_0700_time } + let(:method) { :time_start } + let(:expected) { friday_2300_time } + end + end + + describe '#next_time_start' do + let(:next_friday_2300_time) { Time.utc(2020, 4, 17, 23, 0) } + + it_behaves_like 'a freeze period method' do + let(:now) { monday_0700_time } + let(:method) { :next_time_start } + let(:expected) { next_friday_2300_time } + end + end + + describe '#time_end_from_now' do + it_behaves_like 'a freeze period method' do + let(:now) { saturday_1200_time } + let(:method) { :time_end_from_now } + let(:expected) { monday_0700_time } + end + end + + describe '#time_end_from_start' do + it_behaves_like 'a freeze period method' do + let(:now) { saturday_1200_time } + let(:method) { :time_end_from_start } + let(:expected) { monday_0700_time } + end + end end diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb deleted file mode 100644 index ecbb7af64f7..00000000000 --- a/spec/models/ci/freeze_period_status_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -RSpec.describe Ci::FreezePeriodStatus do - let(:project) { create :project } - # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" - let(:friday_2300) { '0 23 * * 5' } - let(:monday_0700) { '0 7 * * 1' } - - subject { described_class.new(project: project).execute } - - shared_examples 'within freeze period' do |time| - it 'is frozen' do - travel_to(time) do - expect(subject).to be_truthy - end - end - end - - shared_examples 'outside freeze period' do |time| - it 'is not frozen' do - travel_to(time) do - expect(subject).to be_falsy - end - end - end - - describe 'single freeze period' do - let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) } - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1) - end - - describe 'multiple freeze periods' do - # '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday."" - let(:friday_2330) { '30 23 * * 5' } - let(:monday_0800) { '0 8 * * 1' } - - let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) } - let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) } - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59) - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1) - end - - # https://gitlab.com/gitlab-org/gitlab/-/issues/370472 - context 'when period overlaps with itself' do - let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *') } - - it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0) - - it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0) - end -end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index cc48c627049..2670127442e 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -2010,4 +2010,23 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do end end end + + describe '#deploy_freezes' do + let(:environment) { create(:environment, project: project, name: 'staging') } + let(:freeze_period) { create(:ci_freeze_period, project: project) } + + subject { environment.deploy_freezes } + + it 'returns the freeze periods of the associated project' do + expect(subject).to contain_exactly(freeze_period) + end + + it 'caches the freeze periods' do + expect(Gitlab::SafeRequestStore).to receive(:fetch) + .at_least(:once) + .and_return([freeze_period]) + + subject + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9a4179b9157..04edb755b58 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8482,6 +8482,41 @@ RSpec.describe Project, factory_default: :keep do it_behaves_like 'cascading settings', :only_allow_merge_if_all_discussions_are_resolved end + describe '#archived' do + it { expect(subject.archived).to be_falsey } + it { expect(described_class.new(archived: true).archived).to be_truthy } + end + + describe '#resolve_outdated_diff_discussions' do + it { expect(subject.resolve_outdated_diff_discussions).to be_falsey } + + context 'when set explicitly' do + subject { described_class.new(resolve_outdated_diff_discussions: true) } + + it { expect(subject.resolve_outdated_diff_discussions).to be_truthy } + end + end + + describe '#only_allow_merge_if_all_discussions_are_resolved' do + it { expect(subject.only_allow_merge_if_all_discussions_are_resolved).to be_falsey } + + context 'when set explicitly' do + subject { described_class.new(only_allow_merge_if_all_discussions_are_resolved: true) } + + it { expect(subject.only_allow_merge_if_all_discussions_are_resolved).to be_truthy } + end + end + + describe '#remove_source_branch_after_merge' do + it { expect(subject.remove_source_branch_after_merge).to be_truthy } + + context 'when set explicitly' do + subject { described_class.new(remove_source_branch_after_merge: false) } + + it { expect(subject.remove_source_branch_after_merge).to be_falsey } + end + end + private def finish_job(export_job) diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 341f9a9c60f..1c34936c5c2 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe WorkItem do +RSpec.describe WorkItem, feature_category: :portfolio_management do let_it_be(:reusable_project) { create(:project) } describe 'associations' do @@ -176,4 +176,59 @@ RSpec.describe WorkItem do end end end + + context 'with hierarchy' do + let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) } + let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) } + let_it_be(:type3) { create(:work_item_type, namespace: reusable_project.namespace) } + let_it_be(:type4) { create(:work_item_type, namespace: reusable_project.namespace) } + let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) } + let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type2) } + let_it_be(:hierarchy_restriction3) { create(:hierarchy_restriction, parent_type: type2, child_type: type3) } + let_it_be(:hierarchy_restriction4) { create(:hierarchy_restriction, parent_type: type3, child_type: type3) } + let_it_be(:hierarchy_restriction5) { create(:hierarchy_restriction, parent_type: type3, child_type: type4) } + let_it_be(:item1) { create(:work_item, work_item_type: type1, project: reusable_project) } + let_it_be(:item2_1) { create(:work_item, work_item_type: type2, project: reusable_project) } + let_it_be(:item2_2) { create(:work_item, work_item_type: type2, project: reusable_project) } + let_it_be(:item3_1) { create(:work_item, work_item_type: type3, project: reusable_project) } + let_it_be(:item3_2) { create(:work_item, work_item_type: type3, project: reusable_project) } + let_it_be(:item4) { create(:work_item, work_item_type: type4, project: reusable_project) } + let_it_be(:ignored_ancestor) { create(:work_item, work_item_type: type1, project: reusable_project) } + let_it_be(:ignored_descendant) { create(:work_item, work_item_type: type4, project: reusable_project) } + let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2_1) } + let_it_be(:link2) { create(:parent_link, work_item_parent: item2_1, work_item: item2_2) } + let_it_be(:link3) { create(:parent_link, work_item_parent: item2_2, work_item: item3_1) } + let_it_be(:link4) { create(:parent_link, work_item_parent: item3_1, work_item: item3_2) } + let_it_be(:link5) { create(:parent_link, work_item_parent: item3_2, work_item: item4) } + + describe '#ancestors' do + it 'returns all ancestors in ascending order' do + expect(item3_1.ancestors).to eq([item2_2, item2_1, item1]) + end + + it 'returns an empty array if there are no ancestors' do + expect(item1.ancestors).to be_empty + end + end + + describe '#same_type_base_and_ancestors' do + it 'returns self and all ancestors of the same type in ascending order' do + expect(item3_2.same_type_base_and_ancestors).to eq([item3_2, item3_1]) + end + + it 'returns self if there are no ancestors of the same type' do + expect(item3_1.same_type_base_and_ancestors).to match_array([item3_1]) + end + end + + describe '#same_type_descendants_depth' do + it 'returns max descendants depth including self' do + expect(item3_1.same_type_descendants_depth).to eq(2) + end + + it 'returns 1 if there are no descendants' do + expect(item1.same_type_descendants_depth).to eq(1) + end + end + end end diff --git a/spec/models/work_items/parent_link_spec.rb b/spec/models/work_items/parent_link_spec.rb index a2e6ba06199..f6d325c62e7 100644 --- a/spec/models/work_items/parent_link_spec.rb +++ b/spec/models/work_items/parent_link_spec.rb @@ -89,6 +89,55 @@ RSpec.describe WorkItems::ParentLink, feature_category: :portfolio_management do end end + context 'with nested ancestors' do + let_it_be(:type1) { create(:work_item_type, namespace: project.namespace) } + let_it_be(:type2) { create(:work_item_type, namespace: project.namespace) } + let_it_be(:item1) { create(:work_item, work_item_type: type1, project: project) } + let_it_be(:item2) { create(:work_item, work_item_type: type2, project: project) } + let_it_be(:item3) { create(:work_item, work_item_type: type2, project: project) } + let_it_be(:item4) { create(:work_item, work_item_type: type2, project: project) } + let_it_be(:hierarchy_restriction1) { create(:hierarchy_restriction, parent_type: type1, child_type: type2) } + let_it_be(:hierarchy_restriction2) { create(:hierarchy_restriction, parent_type: type2, child_type: type1) } + + let_it_be(:hierarchy_restriction3) do + create(:hierarchy_restriction, parent_type: type2, child_type: type2, maximum_depth: 2) + end + + let_it_be(:link1) { create(:parent_link, work_item_parent: item1, work_item: item2) } + let_it_be(:link2) { create(:parent_link, work_item_parent: item3, work_item: item4) } + + describe '#validate_depth' do + it 'is valid if depth is in limit' do + link = build(:parent_link, work_item_parent: item1, work_item: item3) + + expect(link).to be_valid + end + + it 'is not valid when maximum depth is reached' do + link = build(:parent_link, work_item_parent: item2, work_item: item3) + + expect(link).not_to be_valid + expect(link.errors[:work_item]).to include('reached maximum depth') + end + end + + describe '#validate_cyclic_reference' do + it 'is not valid if parent and child are same' do + link1.work_item_parent = item2 + + expect(link1).not_to be_valid + expect(link1.errors[:work_item]).to include('is not allowed to point to itself') + end + + it 'is not valid if child is already in ancestors' do + link = build(:parent_link, work_item_parent: item4, work_item: item3) + + expect(link).not_to be_valid + expect(link.errors[:work_item]).to include('is already present in ancestors') + end + end + end + it 'is not valid if parent is in other project' do link = build(:parent_link, work_item_parent: task1, work_item: build(:work_item)) diff --git a/spec/presenters/ci/freeze_period_presenter_spec.rb b/spec/presenters/ci/freeze_period_presenter_spec.rb new file mode 100644 index 00000000000..e9959540b8d --- /dev/null +++ b/spec/presenters/ci/freeze_period_presenter_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::FreezePeriodPresenter, feature_category: :release_orchestration do + let_it_be(:project) { build_stubbed(:project) } + + let(:presenter) { described_class.new(freeze_period) } + + describe '#start_time' do + let(:freeze_period) { build_stubbed(:ci_freeze_period, project: project) } + + context 'when active' do + # Default freeze period factory is on a weekend, so let's travel in time to a Saturday! + let(:time) { Time.utc(2022, 12, 3, 6) } + let(:previous_start) { Time.utc(2022, 12, 2, 23) } + + it 'returns the previous time of the freeze period start' do + travel_to(time) do + expect(presenter.start_time).to eq(previous_start) + end + end + end + + context 'when inactive' do + # Default freeze period factory is on a weekend, so we travel back a couple of days earlier. + let(:time) { Time.utc(2022, 11, 30, 6) } + let(:next_start) { Time.utc(2022, 12, 2, 23) } + + it 'returns the next time of the freeze period start' do + travel_to(time) do + expect(presenter.start_time).to eq(next_start) + end + end + end + end +end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index 8e4b905791c..99eef3564a4 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -93,6 +93,11 @@ RSpec.describe 'getting a work item list for a project' do } ... on WorkItemWidgetHierarchy { parent { id } + children { + nodes { + id + } + } } ... on WorkItemWidgetLabels { labels { nodes { id } } diff --git a/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb b/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb index 942e27f2559..4387a3f5b07 100644 --- a/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb +++ b/spec/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml_spec.rb @@ -12,5 +12,9 @@ RSpec.describe 'shared/gitlab_version/_security_patch_upgrade_alert' do it 'renders the security patch upgrade alert' do expect(rendered).to have_selector('#js-security-patch-upgrade-alert') end + + it 'renders the security patch upgrade alert modal' do + expect(rendered).to have_selector('#js-security-patch-upgrade-alert-modal') + end end end |
