From b17f0b91a66f2101a54dd1efed0c4973f04b1daf Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 12 Oct 2020 15:08:32 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/global.gitlab-ci.yml | 6 +- .rubocop_todo.yml | 5 - Gemfile | 4 +- Gemfile.lock | 13 +- .../behaviors/shortcuts/keybindings.js | 96 ++++++ .../behaviors/shortcuts/shortcuts.js | 3 +- .../components/list/item.vue | 8 +- .../diffs/components/diff_row_utils.js | 18 +- .../components/inline_diff_table_row.vue | 6 +- .../components/parallel_diff_table_row.vue | 6 +- .../add_extra_tokens_for_merge_requests.js | 26 +- .../components/issuable_header.vue | 120 ++++++++ .../javascripts/lib/utils/datetime_utility.js | 15 + .../pages/dashboard/merge_requests/index.js | 2 +- app/assets/stylesheets/pages/diff.scss | 16 + .../stylesheets/pages/merge_requests.scss | 16 + app/controllers/concerns/snippets_actions.rb | 40 +-- .../merge_requests/diffs_controller.rb | 1 - .../projects/merge_requests_controller.rb | 1 - .../projects/snippets_controller.rb | 21 +- app/controllers/snippets_controller.rb | 23 +- .../container_expiration_policies_helper.rb | 5 + app/models/ci/build.rb | 10 - app/models/environment_status.rb | 8 - app/serializers/discussion_entity.rb | 5 +- .../mergeability_check_service.rb | 2 - app/services/notes/create_service.rb | 2 +- app/views/dashboard/merge_requests.html.haml | 2 +- app/views/projects/compare/_form.html.haml | 8 +- .../projects/registry/settings/_index.haml | 2 +- .../shared/issuable/_search_bar.html.haml | 12 +- app/workers/git_garbage_collect_worker.rb | 2 +- .../unreleased/-231207-projects-compare.yml | 5 + ...llow-optional-caching-in-failed-builds.yml | 5 + ...-old-projects-to-have-a-cleanup-policy.yml | 5 + .../unreleased/mb_rails_save_bang_fix3.yml | 5 + .../ph-207481-targetBranchFilterDashboard.yml | 5 + .../ph-218300-fixedSystemHeaderOnDiffs.yml | 5 + config/environments/production.rb | 2 +- .../additional_snowplow_tracking.yml | 2 +- .../ci_child_of_child_pipeline.yml | 2 +- .../feature_flags/development/ci_lint_vue.yml | 2 +- ...ner_expiration_policies_historic_entry.yml | 7 + .../deploy_boards_dedupe_instances.yml | 2 +- .../drop_license_management_artifact.yml | 2 +- .../development/ingress_modsecurity.yml | 2 +- .../junit_pipeline_screenshots_view.yml | 2 +- .../development/merge_ref_head_comments.yml | 7 - .../development/modifed_path_ci_variables.yml | 7 - .../development/product_analytics.yml | 2 +- .../project_finder_similarity_sort.yml | 2 +- .../push_rules_supersede_code_owners.yml | 7 + .../development/rebalance_issues.yml | 2 +- .../development/save_raw_usage_data.yml | 2 +- .../track_issue_activity_actions.yml | 2 +- .../development/usage_data_api.yml | 2 +- ...e_data_i_source_code_code_intelligence.yml | 2 +- config/initializers/sprockets.rb | 1 + config/redis.cache.yml.example | 2 +- config/redis.queues.yml.example | 2 +- config/redis.shared_state.yml.example | 2 +- config/resque.yml.example | 2 +- config/routes/project.rb | 2 +- config/routes/snippets.rb | 2 +- .../development/29_instance_statistics.rb | 26 +- doc/ci/yaml/README.md | 30 +- doc/development/fe_guide/index.md | 4 + .../fe_guide/keyboard_shortcuts.md | 98 ++++++ doc/user/markdown.md | 44 ++- doc/user/packages/container_registry/index.md | 14 + lib/api/entities/job_request/cache.rb | 2 +- lib/gitlab/ci/config/entry/cache.rb | 24 +- lib/gitlab/ci/config/entry/needs.rb | 23 +- lib/gitlab/ci/config/entry/ports.rb | 26 +- lib/gitlab/ci/config/entry/rules.rb | 21 +- lib/gitlab/ci/config/entry/services.rb | 26 +- lib/gitlab/ci/pipeline/seed/build/cache.rb | 4 +- lib/gitlab/config/entry/composable_array.rb | 58 ++++ lib/gitlab/config/entry/composable_hash.rb | 2 +- lib/system_check/app/redis_version_check.rb | 2 +- lib/tasks/gettext.rake | 2 +- locale/gitlab.pot | 6 + .../fixtures/designs}/banana_sample.gif | Bin .../fixtures/designs}/tanuki.jpg | Bin qa/qa/fixtures/designs/update/tanuki.jpg | Bin 0 -> 83907 bytes .../fixtures/designs}/values.png | Bin qa/qa/page/component/design_management.rb | 14 + qa/qa/resource/design.rb | 17 +- .../2_plan/issue/create_issue_spec.rb | 2 +- .../add_design_content_spec.rb | 2 +- .../modify_design_content_spec.rb | 30 ++ .../projects/snippets_controller_spec.rb | 283 +---------------- spec/controllers/snippets_controller_spec.rb | 290 +----------------- spec/factories/ci/builds.rb | 3 +- .../features/dashboard/merge_requests_spec.rb | 6 + .../settings/registry_settings_spec.rb | 70 ++++- .../behaviors/shortcuts/keybindings_spec.js | 66 ++++ .../components/inline_diff_table_row_spec.js | 31 +- .../parallel_diff_table_row_spec.js | 26 +- .../components/issuable_header_spec.js | 132 ++++++++ spec/frontend/issuable_show/mock_data.js | 33 ++ .../lib/utils/datetime_utility_spec.js | 28 ++ ...ntainer_expiration_policies_helper_spec.rb | 25 ++ spec/lib/gitlab/ci/config/entry/cache_spec.rb | 110 +++++-- spec/lib/gitlab/ci/config/entry/job_spec.rb | 4 +- spec/lib/gitlab/ci/config/entry/root_spec.rb | 12 +- .../ci/pipeline/seed/build/cache_spec.rb | 3 +- spec/lib/gitlab/ci/yaml_processor_spec.rb | 18 +- .../config/entry/composable_array_spec.rb | 69 +++++ spec/models/ci/build_spec.rb | 88 ------ spec/models/environment_status_spec.rb | 12 - .../api/ci/runner/jobs_request_post_spec.rb | 3 +- spec/routing/project_routing_spec.rb | 15 - spec/routing/routing_spec.rb | 15 - spec/serializers/discussion_entity_spec.rb | 8 - .../ci/create_pipeline_service/cache_spec.rb | 15 +- .../create_from_issue_service_spec.rb | 2 +- .../mergeability_check_service_spec.rb | 10 - spec/services/notes/create_service_spec.rb | 8 - .../migrations_helpers/cluster_helpers.rb | 10 +- .../migrations_helpers/namespaces_helper.rb | 2 +- .../shared_contexts/email_shared_context.rb | 2 +- .../group_projects_finder_shared_contexts.rb | 6 +- .../mailers/notify_shared_context.rb | 2 +- .../git_garbage_collect_worker_spec.rb | 8 +- 125 files changed, 1345 insertions(+), 1152 deletions(-) create mode 100644 app/assets/javascripts/behaviors/shortcuts/keybindings.js create mode 100644 app/assets/javascripts/issuable_show/components/issuable_header.vue create mode 100644 changelogs/unreleased/-231207-projects-compare.yml create mode 100644 changelogs/unreleased/18969-allow-optional-caching-in-failed-builds.yml create mode 100644 changelogs/unreleased/244050-feature-flag-to-allow-old-projects-to-have-a-cleanup-policy.yml create mode 100644 changelogs/unreleased/mb_rails_save_bang_fix3.yml create mode 100644 changelogs/unreleased/ph-207481-targetBranchFilterDashboard.yml create mode 100644 changelogs/unreleased/ph-218300-fixedSystemHeaderOnDiffs.yml create mode 100644 config/feature_flags/development/container_expiration_policies_historic_entry.yml delete mode 100644 config/feature_flags/development/merge_ref_head_comments.yml delete mode 100644 config/feature_flags/development/modifed_path_ci_variables.yml create mode 100644 config/feature_flags/development/push_rules_supersede_code_owners.yml create mode 100644 config/initializers/sprockets.rb create mode 100644 doc/development/fe_guide/keyboard_shortcuts.md create mode 100644 lib/gitlab/config/entry/composable_array.rb rename qa/{spec/fixtures => qa/fixtures/designs}/banana_sample.gif (100%) rename qa/{spec/fixtures => qa/fixtures/designs}/tanuki.jpg (100%) create mode 100644 qa/qa/fixtures/designs/update/tanuki.jpg rename qa/{spec/fixtures => qa/fixtures/designs}/values.png (100%) create mode 100644 qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb create mode 100644 spec/frontend/behaviors/shortcuts/keybindings_spec.js create mode 100644 spec/frontend/issuable_show/components/issuable_header_spec.js create mode 100644 spec/frontend/issuable_show/mock_data.js create mode 100644 spec/lib/gitlab/config/entry/composable_array_spec.rb diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 181e8c8811c..fea3956bfe8 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -94,7 +94,8 @@ - name: postgres:11.6 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:4.0-alpine - - name: elasticsearch:6.4.2 + - name: elasticsearch:7.9.2 + command: ["elasticsearch", "-E", "discovery.type=single-node"] variables: POSTGRES_HOST_AUTH_METHOD: trust @@ -104,7 +105,8 @@ - name: postgres:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:4.0-alpine - - name: elasticsearch:6.4.2 + - name: elasticsearch:7.9.2 + command: ["elasticsearch", "-E", "discovery.type=single-node"] variables: POSTGRES_HOST_AUTH_METHOD: trust diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 70fc7c8d6c3..84c4fe93ed8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1165,11 +1165,6 @@ Rails/SaveBang: - 'spec/services/users/repair_ldap_blocked_service_spec.rb' - 'spec/services/verify_pages_domain_service_spec.rb' - 'spec/sidekiq/cron/job_gem_dependency_spec.rb' - - 'spec/support/migrations_helpers/cluster_helpers.rb' - - 'spec/support/migrations_helpers/namespaces_helper.rb' - - 'spec/support/shared_contexts/email_shared_context.rb' - - 'spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb' - - 'spec/support/shared_contexts/mailers/notify_shared_context.rb' # Offense count: 187 # Cop supports --auto-correct. diff --git a/Gemfile b/Gemfile index faaa8290d1d..d426ad42403 100644 --- a/Gemfile +++ b/Gemfile @@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2' gem 'rack-proxy', '~> 0.6.0' gem 'sassc-rails', '~> 2.1.0' -gem 'uglifier', '~> 2.7.2' +gem 'terser', '~> 1.0' gem 'addressable', '~> 2.7' gem 'font-awesome-rails', '~> 4.7' @@ -430,7 +430,7 @@ end gem 'octokit', '~> 4.15' # https://gitlab.com/gitlab-org/gitlab/issues/207207 -gem 'gitlab-mail_room', '~> 0.0.6', require: 'mail_room' +gem 'gitlab-mail_room', '~> 0.0.7', require: 'mail_room' gem 'email_reply_trimmer', '~> 0.1' gem 'html2text' diff --git a/Gemfile.lock b/Gemfile.lock index 7a24130096b..97aa43db2a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -312,7 +312,7 @@ GEM tzinfo eventmachine (1.2.7) excon (0.71.1) - execjs (2.6.0) + execjs (2.7.0) expression_parser (0.9.0) extended-markdown-filter (0.6.0) html-pipeline (~> 2.0) @@ -436,7 +436,7 @@ GEM opentracing (~> 0.4) redis (> 3.0.0, < 5.0.0) gitlab-license (1.0.0) - gitlab-mail_room (0.0.6) + gitlab-mail_room (0.0.7) gitlab-markup (1.7.1) gitlab-net-dns (0.9.1) gitlab-puma (4.3.5.gitlab.3) @@ -1130,6 +1130,8 @@ GEM temple (0.8.2) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) + terser (1.0.1) + execjs (>= 0.3.0, < 3) test-prof (0.12.0) text (1.3.1) thin (1.7.2) @@ -1157,9 +1159,6 @@ GEM thread_safe (~> 0.1) u2f (0.2.1) uber (0.1.0) - uglifier (2.7.2) - execjs (>= 0.3.0) - json (>= 1.8.0) unf (0.1.4) unf_ext unf_ext (0.0.7.5) @@ -1327,7 +1326,7 @@ DEPENDENCIES gitlab-fog-azure-rm (~> 1.0) gitlab-labkit (= 0.12.1) gitlab-license (~> 1.0) - gitlab-mail_room (~> 0.0.6) + gitlab-mail_room (~> 0.0.7) gitlab-markup (~> 1.7.1) gitlab-net-dns (~> 0.9.1) gitlab-puma (~> 4.3.3.gitlab.2) @@ -1483,13 +1482,13 @@ DEPENDENCIES stackprof (~> 0.2.15) state_machines-activerecord (~> 0.6.0) sys-filesystem (~> 1.1.6) + terser (~> 1.0) test-prof (~> 0.12.0) thin (~> 1.7.0) timecop (~> 0.9.1) toml-rb (~> 1.0.0) truncato (~> 0.7.11) u2f (~> 0.2.1) - uglifier (~> 2.7.2) unf (~> 0.1.4) unicorn (~> 5.5) unicorn-worker-killer (~> 0.4.4) diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js new file mode 100644 index 00000000000..bbcc40ab9fe --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -0,0 +1,96 @@ +import { flatten } from 'lodash'; +import { s__ } from '~/locale'; +import AccessorUtilities from '~/lib/utils/accessor'; +import { shouldDisableShortcuts } from './shortcuts_toggle'; + +export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations'; + +let parsedCustomizations = {}; +const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe(); + +if (localStorageIsSafe) { + try { + parsedCustomizations = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '{}'); + } catch (e) { + /* do nothing */ + } +} + +/** + * A map of command => keys of all keyboard shortcuts + * that have been customized by the user. + * + * @example + * { "globalShortcuts.togglePerformanceBar": ["p e r f"] } + * + * @type { Object. } + */ +export const customizations = parsedCustomizations; + +// All available commands +export const TOGGLE_PERFORMANCE_BAR = 'globalShortcuts.togglePerformanceBar'; + +/** All keybindings, grouped and ordered with descriptions */ +export const keybindingGroups = [ + { + groupId: 'globalShortcuts', + name: s__('KeyboardShortcuts|Global Shortcuts'), + keybindings: [ + { + description: s__('KeyboardShortcuts|Toggle the Performance Bar'), + command: TOGGLE_PERFORMANCE_BAR, + // eslint-disable-next-line @gitlab/require-i18n-strings + defaultKeys: ['p b'], + }, + ], + }, +] + + // For each keybinding object, add a `customKeys` property populated with the + // user's custom keybindings (if the command has been customized). + // `customKeys` will be `undefined` if the command hasn't been customized. + .map(group => { + return { + ...group, + keybindings: group.keybindings.map(binding => ({ + ...binding, + customKeys: customizations[binding.command], + })), + }; + }); + +/** + * A simple map of command => keys. All user customizations are included in this map. + * This mapping is used to simplify `keysFor` below. + * + * @example + * { "globalShortcuts.togglePerformanceBar": ["p e r f"] } + */ +const commandToKeys = flatten(keybindingGroups.map(group => group.keybindings)).reduce( + (acc, binding) => { + acc[binding.command] = binding.customKeys || binding.defaultKeys; + return acc; + }, + {}, +); + +/** + * Gets keyboard shortcuts associated with a command + * + * @param {string} command The command string. All command + * strings are available as imports from this file. + * + * @returns {string[]} An array of keyboard shortcut strings bound to the command + * + * @example + * import { keysFor, TOGGLE_PERFORMANCE_BAR } from '~/behaviors/shortcuts/keybindings' + * + * Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), handler); + */ +export const keysFor = command => { + if (shouldDisableShortcuts()) { + return []; + } + + return commandToKeys[command]; +}; diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 3cb2d6719c8..a53150f8d61 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -9,6 +9,7 @@ import axios from '../../lib/utils/axios_utils'; import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils'; +import { keysFor, TOGGLE_PERFORMANCE_BAR } from './keybindings'; const defaultStopCallback = Mousetrap.prototype.stopCallback; Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { @@ -70,7 +71,7 @@ export default class Shortcuts { Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('/', Shortcuts.focusSearch); Mousetrap.bind('f', this.focusFilter.bind(this)); - Mousetrap.bind('p b', Shortcuts.onTogglePerfBar); + Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), Shortcuts.onTogglePerfBar); const findFileURL = document.body.dataset.findFile; diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue index b179b1b5e79..fa09c7c15cc 100644 --- a/app/assets/javascripts/design_management/components/list/item.vue +++ b/app/assets/javascripts/design_management/components/list/item.vue @@ -132,7 +132,13 @@ export default { >
- +
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js index 998320c3245..08b87a4bade 100644 --- a/app/assets/javascripts/diffs/components/diff_row_utils.js +++ b/app/assets/javascripts/diffs/components/diff_row_utils.js @@ -1,4 +1,3 @@ -import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import { MATCH_LINE_TYPE, @@ -23,21 +22,8 @@ export const isMatchLine = type => type === MATCH_LINE_TYPE; export const isMetaLine = type => [OLD_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE, EMPTY_CELL_TYPE].includes(type); -export const shouldRenderCommentButton = ( - isLoggedIn, - isCommentButtonRendered, - featureMergeRefHeadComments = false, -) => { - if (!isCommentButtonRendered) { - return false; - } - - if (isLoggedIn) { - const isDiffHead = parseBoolean(getParameterByName('diff_head')); - return !isDiffHead || featureMergeRefHeadComments; - } - - return false; +export const shouldRenderCommentButton = (isLoggedIn, isCommentButtonRendered) => { + return isCommentButtonRendered && isLoggedIn; }; export const hasDiscussions = line => line?.discussions?.length > 0; diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index f9d491603cb..99cf79a70d4 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -81,11 +81,7 @@ export default { return utils.addCommentTooltip(this.line); }, shouldRenderCommentButton() { - return utils.shouldRenderCommentButton( - this.isLoggedIn, - true, - gon.features?.mergeRefHeadComments, - ); + return utils.shouldRenderCommentButton(this.isLoggedIn, true); }, shouldShowCommentButton() { return utils.shouldShowCommentButton( diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index 06dcadb2dc1..cdc6db791f0 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -102,11 +102,7 @@ export default { return utils.addCommentTooltip(this.line.right); }, shouldRenderCommentButton() { - return utils.shouldRenderCommentButton( - this.isLoggedIn, - this.isCommentButtonRendered, - gon.features?.mergeRefHeadComments, - ); + return utils.shouldRenderCommentButton(this.isLoggedIn, this.isCommentButtonRendered); }, shouldShowCommentButtonLeft() { return utils.shouldShowCommentButton( diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js index 1bfbab9ef96..f8b47727921 100644 --- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js +++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js @@ -1,6 +1,6 @@ import { __ } from '~/locale'; -export default IssuableTokenKeys => { +export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { const draftToken = { token: { formattedKey: __('Draft'), @@ -51,18 +51,20 @@ export default IssuableTokenKeys => { IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token); IssuableTokenKeys.conditions.push(...draftToken.conditions); - const targetBranchToken = { - formattedKey: __('Target-Branch'), - key: 'target-branch', - type: 'string', - param: '', - symbol: '', - icon: 'arrow-right', - tag: 'branch', - }; + if (!disableTargetBranchFilter) { + const targetBranchToken = { + formattedKey: __('Target-Branch'), + key: 'target-branch', + type: 'string', + param: '', + symbol: '', + icon: 'arrow-right', + tag: 'branch', + }; - IssuableTokenKeys.tokenKeys.push(targetBranchToken); - IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken); + IssuableTokenKeys.tokenKeys.push(targetBranchToken); + IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken); + } const approvedBy = { token: { diff --git a/app/assets/javascripts/issuable_show/components/issuable_header.vue b/app/assets/javascripts/issuable_show/components/issuable_header.vue new file mode 100644 index 00000000000..3815c50cac6 --- /dev/null +++ b/app/assets/javascripts/issuable_show/components/issuable_header.vue @@ -0,0 +1,120 @@ + + + diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 261f76a0f2d..103ea839a4b 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -85,6 +85,21 @@ export const getDayName = date => __('Saturday'), ][date.getDay()]; +/** + * Returns the i18n month name from a given date + * @example + * formatDateAsMonth(new Date('2020-06-28')) -> 'Jun' + * @param {String} datetime where month is extracted from + * @param {Object} options + * @param {Boolean} options.abbreviated whether to use the abbreviated month string, or not + * @return {String} the i18n month name + */ +export function formatDateAsMonth(datetime, options = {}) { + const { abbreviated = true } = options; + const month = new Date(datetime).getMonth(); + return getMonthNames(abbreviated)[month]; +} + /** * @example * dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000" diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index 10df18c85e7..7adae2cdb05 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -5,7 +5,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { - addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); + addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 3c432fe09c0..7d622dda070 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -38,6 +38,14 @@ top: $mr-file-header-top; z-index: 120; + .with-system-header & { + top: $mr-file-header-top + $system-header-height; + } + + .with-system-header.with-performance-bar & { + top: $mr-file-header-top + $system-header-height + $performance-bar-height; + } + &::before { content: ''; position: absolute; @@ -1078,6 +1086,14 @@ table.code { max-height: calc(100vh - #{$top-pos}); z-index: 202; + .with-system-header & { + top: $top-pos + $system-header-height; + } + + .with-system-header.with-performance-bar & { + top: $top-pos + $system-header-height + $performance-bar-height; + } + .with-performance-bar & { $performance-bar-top-pos: $performance-bar-height + $top-pos; top: $performance-bar-top-pos; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ddec04b1b0c..5835f665ada 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -771,6 +771,14 @@ $mr-widget-min-height: 69px; position: sticky; top: $header-height + $mr-tabs-height; + .with-system-header & { + top: $header-height + $mr-tabs-height + $system-header-height; + } + + .with-system-header.with-performance-bar & { + top: $header-height + $mr-tabs-height + $system-header-height + $performance-bar-height; + } + .mr-version-menus-container { flex-wrap: nowrap; } @@ -788,6 +796,14 @@ $mr-widget-min-height: 69px; background-color: $white; border-bottom: 1px solid $border-color; + .with-system-header & { + top: $header-height + $system-header-height; + } + + .with-system-header.with-performance-bar & { + top: $header-height + $system-header-height + $performance-bar-height; + } + @include media-breakpoint-up(sm) { position: -webkit-sticky; position: sticky; diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index 4548595d968..e4c3df6ccc3 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -17,13 +17,7 @@ module SnippetsActions respond_to :html end - def edit - # We need to load some info from the existing blob - snippet.content = blob.data - snippet.file_name = blob.path - - render 'edit' - end + def edit; end # This endpoint is being replaced by Snippets::BlobController#raw # Support for old raw links will be maintainted via this action but @@ -55,7 +49,6 @@ module SnippetsActions def show respond_to do |format| format.html do - conditionally_expand_blob(blob) @note = Note.new(noteable: @snippet, project: @snippet.project) @noteable = @snippet @@ -80,29 +73,6 @@ module SnippetsActions end end end - - def update - update_params = snippet_params.merge(spammable_params) - - service_response = Snippets::UpdateService.new(@snippet.project, current_user, update_params).execute(@snippet) - @snippet = service_response.payload[:snippet] - - handle_repository_error(:edit) - end - - def destroy - service_response = Snippets::DestroyService.new(current_user, @snippet).execute - - if service_response.success? - redirect_to gitlab_dashboard_snippets_path(@snippet), status: :found - elsif service_response.http_status == 403 - access_denied! - else - redirect_to gitlab_snippet_path(@snippet), - status: :found, - alert: service_response.message - end - end # rubocop:enable Gitlab/ModuleWithInstanceVariables private @@ -124,12 +94,4 @@ module SnippetsActions def convert_line_endings(content) params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n") end - - def handle_repository_error(action) - errors = Array(snippet.errors.delete(:repository)) - - flash.now[:alert] = errors.first if errors.present? - - recaptcha_check_with_fallback(errors.empty?) { render action } - end end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 28aef6f4328..07c38431f0f 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -173,7 +173,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic end def update_diff_discussion_positions! - return unless Feature.enabled?(:merge_ref_head_comments, @merge_request.target_project, default_enabled: true) return unless Feature.enabled?(:merge_red_head_comments_position_on_demand, @merge_request.target_project, default_enabled: true) return if @merge_request.has_any_diff_note_positions? diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 8ca70602c89..ae055e9494c 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -29,7 +29,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action only: [:show] do push_frontend_experiment(:suggest_pipeline) push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true) - push_frontend_feature_flag(:merge_ref_head_comments, @project, default_enabled: true) push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true) push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true) push_frontend_feature_flag(:file_identifier_hash) diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 49840e847f2..779e149bb9c 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -7,12 +7,11 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController before_action :check_snippets_available! - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] + before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam] - before_action :authorize_create_snippet!, only: [:new, :create] - before_action :authorize_read_snippet!, except: [:new, :create, :index] - before_action :authorize_update_snippet!, only: [:edit, :update] - before_action :authorize_admin_snippet!, only: [:destroy] + before_action :authorize_create_snippet!, only: :new + before_action :authorize_read_snippet!, except: [:new, :index] + before_action :authorize_update_snippet!, only: :edit def index @snippet_counts = ::Snippets::CountService @@ -33,14 +32,6 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController @snippet = @noteable = @project.snippets.build end - def create - create_params = snippet_params.merge(spammable_params) - service_response = ::Snippets::CreateService.new(project, current_user, create_params).execute - @snippet = service_response.payload[:snippet] - - handle_repository_error(:new) - end - protected alias_method :awardable, :snippet @@ -49,8 +40,4 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController def spammable_path project_snippet_path(@project, @snippet) end - - def snippet_params - params.require(:project_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description) - end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index e68b821459d..913b1e3bb6e 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -6,12 +6,11 @@ class SnippetsController < Snippets::ApplicationController include ToggleAwardEmoji include SpammableActions - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] + before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam] - before_action :authorize_create_snippet!, only: [:new, :create] + before_action :authorize_create_snippet!, only: :new before_action :authorize_read_snippet!, only: [:show, :raw] - before_action :authorize_update_snippet!, only: [:edit, :update] - before_action :authorize_admin_snippet!, only: [:destroy] + before_action :authorize_update_snippet!, only: :edit skip_before_action :authenticate_user!, only: [:index, :show, :raw] @@ -40,18 +39,6 @@ class SnippetsController < Snippets::ApplicationController @snippet = PersonalSnippet.new end - def create - create_params = snippet_params.merge(files: params.delete(:files)) - service_response = Snippets::CreateService.new(nil, current_user, create_params).execute - @snippet = service_response.payload[:snippet] - - if service_response.error? && @snippet.errors[:repository].present? - handle_repository_error(:new) - else - recaptcha_check_with_fallback { render :new } - end - end - protected alias_method :awardable, :snippet @@ -60,8 +47,4 @@ class SnippetsController < Snippets::ApplicationController def spammable_path snippet_path(@snippet) end - - def snippet_params - params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description).merge(spammable_params) - end end diff --git a/app/helpers/container_expiration_policies_helper.rb b/app/helpers/container_expiration_policies_helper.rb index cc6d717ce35..52f68ac53f0 100644 --- a/app/helpers/container_expiration_policies_helper.rb +++ b/app/helpers/container_expiration_policies_helper.rb @@ -24,4 +24,9 @@ module ContainerExpirationPoliciesHelper end end end + + def container_expiration_policies_historic_entry_enabled?(project) + Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries || + Feature.enabled?(:container_expiration_policies_historic_entry, project) + end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 21c3ab3eebe..42f326d40dc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -526,7 +526,6 @@ module Ci .concat(job_jwt_variables) .concat(scoped_variables) .concat(job_variables) - .concat(environment_changed_page_variables) .concat(persisted_environment_variables) .to_runner_variables end @@ -563,15 +562,6 @@ module Ci end end - def environment_changed_page_variables - Gitlab::Ci::Variables::Collection.new.tap do |variables| - break variables unless environment_status && Feature.enabled?(:modifed_path_ci_variables, project) - - variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', value: environment_status.changed_paths.join(',')) - variables.append(key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', value: environment_status.changed_urls.join(',')) - end - end - def deploy_token_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| break variables unless gitlab_deploy_token diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 46e41c22139..55ea4e2fe18 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -72,14 +72,6 @@ class EnvironmentStatus .merge_request_diff_files.where(deleted_file: false) end - def changed_paths - changes.map { |change| change[:path] } - end - - def changed_urls - changes.map { |change| change[:external_url] } - end - def has_route_map? project.route_map_for(sha).present? end diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb index 2957205a81c..497471699b2 100644 --- a/app/serializers/discussion_entity.rb +++ b/app/serializers/discussion_entity.rb @@ -69,9 +69,6 @@ class DiscussionEntity < Grape::Entity end def display_merge_ref_discussions?(discussion) - return unless discussion.diff_discussion? - return if discussion.legacy_diff_discussion? - - Feature.enabled?(:merge_ref_head_comments, discussion.project, default_enabled: true) + discussion.diff_discussion? && !discussion.legacy_diff_discussion? end end diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb index 12c04772ef4..b41a5fa317e 100644 --- a/app/services/merge_requests/mergeability_check_service.rb +++ b/app/services/merge_requests/mergeability_check_service.rb @@ -125,8 +125,6 @@ module MergeRequests end def update_diff_discussion_positions! - return if Feature.disabled?(:merge_ref_head_comments, merge_request.target_project, default_enabled: true) - Discussions::CaptureDiffNotePositionsService.new(merge_request).execute end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index e26f662a697..48f44affb23 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -70,7 +70,7 @@ module Notes Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note)) end - if Feature.enabled?(:merge_ref_head_comments, project, default_enabled: true) && note.for_merge_request? && note.diff_note? && note.start_of_discussion? + if note.for_merge_request? && note.diff_note? && note.start_of_discussion? Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion) end end diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index dd9fd34f284..2111b66d26e 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -14,7 +14,7 @@ .top-area = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set -= render 'shared/issuable/search_bar', type: :merge_requests += render 'shared/issuable/search_bar', type: :merge_requests, disable_target_branch: true - if current_user && @no_filters_set = render 'shared/dashboard/no_filter_selected' diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 768acac96c0..a257f2e9433 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,14 +1,14 @@ = form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do - if params[:to] && params[:from] .compare-switch-container - = link_to sprite_icon('substitute'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions' + = link_to sprite_icon('substitute'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn gl-button btn-white', title: 'Swap revisions' .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-prepend .input-group-text = s_("CompareBranches|Source") = hidden_field_tag :to, params[:to] - = button_tag type: 'button', title: params[:to], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do + = button_tag type: 'button', title: params[:to], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do .dropdown-toggle-text.str-truncated.monospace.float-left= params[:to] || _("Select branch/tag") = sprite_icon('chevron-down', css_class: 'float-right') = render 'shared/ref_dropdown' @@ -19,12 +19,12 @@ .input-group-text = s_("CompareBranches|Target") = hidden_field_tag :from, params[:from] - = button_tag type: 'button', title: params[:from], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do + = button_tag type: 'button', title: params[:from], class: "btn gl-button form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do .dropdown-toggle-text.str-truncated.monospace.float-left= params[:from] || _("Select branch/tag") = sprite_icon('chevron-down', css_class: 'float-right') = render 'shared/ref_dropdown'   - = button_tag s_("CompareBranches|Compare"), class: "btn btn-success commits-compare-btn" + = button_tag s_("CompareBranches|Compare"), class: "btn gl-button btn-success commits-compare-btn" - if @merge_request.present? = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'gl-ml-3 btn' - elsif create_mr_button? diff --git a/app/views/projects/registry/settings/_index.haml b/app/views/projects/registry/settings/_index.haml index b53fac83830..c6fae2cc7a1 100644 --- a/app/views/projects/registry/settings/_index.haml +++ b/app/views/projects/registry/settings/_index.haml @@ -5,4 +5,4 @@ older_than_options: older_than_options.to_json, is_admin: current_user&.admin.to_s, admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'), - enable_historic_entries: Gitlab::CurrentSettings.try(:container_expiration_policies_enable_historic_entries).to_s} } + enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s} } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index cd7d792738d..d654bbe0700 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,6 +1,7 @@ - type = local_assigns.fetch(:type) - board = local_assigns.fetch(:board, nil) - show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true) +- disable_target_branch = local_assigns.fetch(:disable_target_branch, false) - placeholder = local_assigns[:placeholder] || _('Search or filter results...') - is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics - block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : '' @@ -154,11 +155,12 @@ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } %button.btn.btn-link{ type: 'button' } = _('No') - #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu - %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } - %li.filter-dropdown-item - %button.btn.btn-link.js-data-value.monospace - {{title}} + - unless disable_target_branch + #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value.monospace + {{title}} = render_if_exists 'shared/issuable/filter_weight', type: type diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index e66bad3962f..9071e4b8a1b 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -100,7 +100,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker end def flush_ref_caches(project) - project.repository.after_create_branch + project.repository.expire_branches_cache project.repository.branch_names project.repository.has_visible_content? end diff --git a/changelogs/unreleased/-231207-projects-compare.yml b/changelogs/unreleased/-231207-projects-compare.yml new file mode 100644 index 00000000000..37ebf5e7e4f --- /dev/null +++ b/changelogs/unreleased/-231207-projects-compare.yml @@ -0,0 +1,5 @@ +--- +title: Apply GitLab UI button styles to buttons in app/views/projects/compare directory +merge_request: 44342 +author: Lakshit +type: other diff --git a/changelogs/unreleased/18969-allow-optional-caching-in-failed-builds.yml b/changelogs/unreleased/18969-allow-optional-caching-in-failed-builds.yml new file mode 100644 index 00000000000..7b55ecc75d8 --- /dev/null +++ b/changelogs/unreleased/18969-allow-optional-caching-in-failed-builds.yml @@ -0,0 +1,5 @@ +--- +title: Add cache:when keyword for ci yml config +merge_request: 41822 +author: +type: added diff --git a/changelogs/unreleased/244050-feature-flag-to-allow-old-projects-to-have-a-cleanup-policy.yml b/changelogs/unreleased/244050-feature-flag-to-allow-old-projects-to-have-a-cleanup-policy.yml new file mode 100644 index 00000000000..1a66f602e54 --- /dev/null +++ b/changelogs/unreleased/244050-feature-flag-to-allow-old-projects-to-have-a-cleanup-policy.yml @@ -0,0 +1,5 @@ +--- +title: Add feature flag for a phased rollout of cleanup policies +merge_request: 44444 +author: +type: added diff --git a/changelogs/unreleased/mb_rails_save_bang_fix3.yml b/changelogs/unreleased/mb_rails_save_bang_fix3.yml new file mode 100644 index 00000000000..af25c188ead --- /dev/null +++ b/changelogs/unreleased/mb_rails_save_bang_fix3.yml @@ -0,0 +1,5 @@ +--- +title: Fix Rails/SaveBang offenses in spec/support/* +merge_request: 44884 +author: matthewbried +type: other diff --git a/changelogs/unreleased/ph-207481-targetBranchFilterDashboard.yml b/changelogs/unreleased/ph-207481-targetBranchFilterDashboard.yml new file mode 100644 index 00000000000..e1eed671357 --- /dev/null +++ b/changelogs/unreleased/ph-207481-targetBranchFilterDashboard.yml @@ -0,0 +1,5 @@ +--- +title: Disable target branch filter option on merge requests dashboard +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/ph-218300-fixedSystemHeaderOnDiffs.yml b/changelogs/unreleased/ph-218300-fixedSystemHeaderOnDiffs.yml new file mode 100644 index 00000000000..01c7d14e600 --- /dev/null +++ b/changelogs/unreleased/ph-218300-fixedSystemHeaderOnDiffs.yml @@ -0,0 +1,5 @@ +--- +title: Fixed merge request tabs overlapping with system header +merge_request: +author: +type: fixed diff --git a/config/environments/production.rb b/config/environments/production.rb index 393a274606e..d9b3ee354b0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -12,7 +12,7 @@ Rails.application.configure do config.public_file_server.enabled = false # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = :terser # config.assets.css_compressor = :sass # Don't fallback to assets pipeline if a precompiled asset is missed diff --git a/config/feature_flags/development/additional_snowplow_tracking.yml b/config/feature_flags/development/additional_snowplow_tracking.yml index 8cff8389dbb..3e2b542b1a8 100644 --- a/config/feature_flags/development/additional_snowplow_tracking.yml +++ b/config/feature_flags/development/additional_snowplow_tracking.yml @@ -1,6 +1,6 @@ name: additional_snowplow_tracking introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12088 rollout_issue_url: -group: group::product_analytics +group: group::product analytics type: development default_enabled: false diff --git a/config/feature_flags/development/ci_child_of_child_pipeline.yml b/config/feature_flags/development/ci_child_of_child_pipeline.yml index 02122076434..7a90334619a 100644 --- a/config/feature_flags/development/ci_child_of_child_pipeline.yml +++ b/config/feature_flags/development/ci_child_of_child_pipeline.yml @@ -2,6 +2,6 @@ name: ci_child_of_child_pipeline introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41102 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/243747 -group: 'group::continuous integration' +group: group::continuous integration type: development default_enabled: true diff --git a/config/feature_flags/development/ci_lint_vue.yml b/config/feature_flags/development/ci_lint_vue.yml index 832f543ba3d..a72e97909be 100644 --- a/config/feature_flags/development/ci_lint_vue.yml +++ b/config/feature_flags/development/ci_lint_vue.yml @@ -2,6 +2,6 @@ name: ci_lint_vue introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42401 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/249661 -group: group::continuous intergration +group: group::continuous integration type: development default_enabled: false \ No newline at end of file diff --git a/config/feature_flags/development/container_expiration_policies_historic_entry.yml b/config/feature_flags/development/container_expiration_policies_historic_entry.yml new file mode 100644 index 00000000000..0525f77eacf --- /dev/null +++ b/config/feature_flags/development/container_expiration_policies_historic_entry.yml @@ -0,0 +1,7 @@ +--- +name: container_expiration_policies_historic_entry +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44444 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/262639 +type: development +group: group::package +default_enabled: false diff --git a/config/feature_flags/development/deploy_boards_dedupe_instances.yml b/config/feature_flags/development/deploy_boards_dedupe_instances.yml index 04cbd6f6602..d407e11babd 100644 --- a/config/feature_flags/development/deploy_boards_dedupe_instances.yml +++ b/config/feature_flags/development/deploy_boards_dedupe_instances.yml @@ -3,5 +3,5 @@ name: deploy_boards_dedupe_instances introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40768 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258214 type: development -group: group::progressive-delivery +group: group::progressive delivery default_enabled: false diff --git a/config/feature_flags/development/drop_license_management_artifact.yml b/config/feature_flags/development/drop_license_management_artifact.yml index cb13486b7f5..34e10fa7ae6 100644 --- a/config/feature_flags/development/drop_license_management_artifact.yml +++ b/config/feature_flags/development/drop_license_management_artifact.yml @@ -2,6 +2,6 @@ name: drop_license_management_artifact introduced_by_url: rollout_issue_url: -group: composition_analysis +group: group::composition analysis type: development default_enabled: true diff --git a/config/feature_flags/development/ingress_modsecurity.yml b/config/feature_flags/development/ingress_modsecurity.yml index a8c8c3b26da..7ed1d089476 100644 --- a/config/feature_flags/development/ingress_modsecurity.yml +++ b/config/feature_flags/development/ingress_modsecurity.yml @@ -2,6 +2,6 @@ name: ingress_modsecurity introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20194 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258554 -group: "group::container security" +group: group::container security type: development default_enabled: false diff --git a/config/feature_flags/development/junit_pipeline_screenshots_view.yml b/config/feature_flags/development/junit_pipeline_screenshots_view.yml index a148c4b70f3..273e0ed450e 100644 --- a/config/feature_flags/development/junit_pipeline_screenshots_view.yml +++ b/config/feature_flags/development/junit_pipeline_screenshots_view.yml @@ -2,6 +2,6 @@ name: junit_pipeline_screenshots_view introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/202114 rollout_issue_url: -group: 'group::verify testing' +group: group::verify testing type: development default_enabled: false diff --git a/config/feature_flags/development/merge_ref_head_comments.yml b/config/feature_flags/development/merge_ref_head_comments.yml deleted file mode 100644 index 6f391860e29..00000000000 --- a/config/feature_flags/development/merge_ref_head_comments.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: merge_ref_head_comments -introduced_by_url: -rollout_issue_url: -group: -type: development -default_enabled: true diff --git a/config/feature_flags/development/modifed_path_ci_variables.yml b/config/feature_flags/development/modifed_path_ci_variables.yml deleted file mode 100644 index a72a5ae56e1..00000000000 --- a/config/feature_flags/development/modifed_path_ci_variables.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: modifed_path_ci_variables -introduced_by_url: -rollout_issue_url: -group: -type: development -default_enabled: false diff --git a/config/feature_flags/development/product_analytics.yml b/config/feature_flags/development/product_analytics.yml index cc1859e149c..02840f3212b 100644 --- a/config/feature_flags/development/product_analytics.yml +++ b/config/feature_flags/development/product_analytics.yml @@ -2,6 +2,6 @@ name: product_analytics introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36443 rollout_issue_url: -group: group::product_analytics +group: group::product analytics type: development default_enabled: false diff --git a/config/feature_flags/development/project_finder_similarity_sort.yml b/config/feature_flags/development/project_finder_similarity_sort.yml index c0460d44b6c..2d29bed82c4 100644 --- a/config/feature_flags/development/project_finder_similarity_sort.yml +++ b/config/feature_flags/development/project_finder_similarity_sort.yml @@ -3,5 +3,5 @@ name: project_finder_similarity_sort introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43136 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263249 type: development -group: group::threat_insights +group: group::threat insights default_enabled: false diff --git a/config/feature_flags/development/push_rules_supersede_code_owners.yml b/config/feature_flags/development/push_rules_supersede_code_owners.yml new file mode 100644 index 00000000000..d185d19522d --- /dev/null +++ b/config/feature_flags/development/push_rules_supersede_code_owners.yml @@ -0,0 +1,7 @@ +--- +name: push_rules_supersede_code_owners +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44126 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/262019 +type: development +group: group::source code +default_enabled: false diff --git a/config/feature_flags/development/rebalance_issues.yml b/config/feature_flags/development/rebalance_issues.yml index 4c14824a35d..df04da8c8d3 100644 --- a/config/feature_flags/development/rebalance_issues.yml +++ b/config/feature_flags/development/rebalance_issues.yml @@ -2,6 +2,6 @@ name: rebalance_issues introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40124 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/239344 -group: 'group::project management' +group: group::project management type: development default_enabled: false diff --git a/config/feature_flags/development/save_raw_usage_data.yml b/config/feature_flags/development/save_raw_usage_data.yml index f71393414ae..b3c65c12e2d 100644 --- a/config/feature_flags/development/save_raw_usage_data.yml +++ b/config/feature_flags/development/save_raw_usage_data.yml @@ -2,6 +2,6 @@ name: save_raw_usage_data introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38457 rollout_issue_url: -group: group::product_analytics +group: group::product analytics type: development default_enabled: false diff --git a/config/feature_flags/development/track_issue_activity_actions.yml b/config/feature_flags/development/track_issue_activity_actions.yml index 034b697ab52..97deb11e2cf 100644 --- a/config/feature_flags/development/track_issue_activity_actions.yml +++ b/config/feature_flags/development/track_issue_activity_actions.yml @@ -2,6 +2,6 @@ name: track_issue_activity_actions introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40904 rollout_issue_url: -group: group::project_management +group: group::project management type: development default_enabled: false \ No newline at end of file diff --git a/config/feature_flags/development/usage_data_api.yml b/config/feature_flags/development/usage_data_api.yml index 139e39807dc..83a08fa3c43 100644 --- a/config/feature_flags/development/usage_data_api.yml +++ b/config/feature_flags/development/usage_data_api.yml @@ -2,6 +2,6 @@ name: usage_data_api introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41301 rollout_issue_url: -group: group::product_analytics +group: group::product analytics type: development default_enabled: false diff --git a/config/feature_flags/development/usage_data_i_source_code_code_intelligence.yml b/config/feature_flags/development/usage_data_i_source_code_code_intelligence.yml index 9ea05b0c7df..15ce7194264 100644 --- a/config/feature_flags/development/usage_data_i_source_code_code_intelligence.yml +++ b/config/feature_flags/development/usage_data_i_source_code_code_intelligence.yml @@ -2,6 +2,6 @@ name: usage_data_i_source_code_code_intelligence introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41881 rollout_issue_url: -group: group::source_code +group: group::source code type: development default_enabled: true diff --git a/config/initializers/sprockets.rb b/config/initializers/sprockets.rb new file mode 100644 index 00000000000..a20b7dc75e9 --- /dev/null +++ b/config/initializers/sprockets.rb @@ -0,0 +1 @@ +Sprockets.register_compressor 'application/javascript', :terser, Terser::Compressor diff --git a/config/redis.cache.yml.example b/config/redis.cache.yml.example index b20f1dd2122..fb92c205ce1 100644 --- a/config/redis.cache.yml.example +++ b/config/redis.cache.yml.example @@ -26,7 +26,7 @@ production: # http://redis.io/topics/sentinel # # You must specify a list of a few sentinels that will handle client connection - # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html + # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html ## # url: redis://master:6380 # sentinels: diff --git a/config/redis.queues.yml.example b/config/redis.queues.yml.example index 46ab39729c4..dd6c10e0e06 100644 --- a/config/redis.queues.yml.example +++ b/config/redis.queues.yml.example @@ -26,7 +26,7 @@ production: # http://redis.io/topics/sentinel # # You must specify a list of a few sentinels that will handle client connection - # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html + # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html ## # url: redis://master:6381 # sentinels: diff --git a/config/redis.shared_state.yml.example b/config/redis.shared_state.yml.example index 05fed947f52..98f6f330bc7 100644 --- a/config/redis.shared_state.yml.example +++ b/config/redis.shared_state.yml.example @@ -26,7 +26,7 @@ production: # http://redis.io/topics/sentinel # # You must specify a list of a few sentinels that will handle client connection - # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html + # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html ## # url: redis://master:6382 # sentinels: diff --git a/config/resque.yml.example b/config/resque.yml.example index 932c1553dfb..0f629a5229c 100644 --- a/config/resque.yml.example +++ b/config/resque.yml.example @@ -22,7 +22,7 @@ production: # http://redis.io/topics/sentinel # # You must specify a list of a few sentinels that will handle client connection - # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html + # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html ## # url: redis://master:6379 # sentinels: diff --git a/config/routes/project.rb b/config/routes/project.rb index 1072a037823..5a30f1026f8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -368,7 +368,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resource :jira, only: [:show], controller: :jira end - resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do + resources :snippets, except: [:create, :update, :destroy], concerns: :awardable, constraints: { id: /\d+/ } do member do get :raw post :mark_as_spam diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 7bb82da4910..9e0c42fa07d 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -1,4 +1,4 @@ -resources :snippets, concerns: :awardable do +resources :snippets, except: [:create, :update, :destroy], concerns: :awardable, constraints: { id: /\d+/ } do member do get :raw post :mark_as_spam diff --git a/db/fixtures/development/29_instance_statistics.rb b/db/fixtures/development/29_instance_statistics.rb index e4ef0f26be0..02afdc61339 100644 --- a/db/fixtures/development/29_instance_statistics.rb +++ b/db/fixtures/development/29_instance_statistics.rb @@ -3,17 +3,31 @@ require './spec/support/sidekiq_middleware' Gitlab::Seeder.quiet do + chance_for_decrement = 0.1 # 10% chance that we'll generate smaller count than the previous count + max_increase = 10000 + max_decrease = 1000 + model_class = Analytics::InstanceStatistics::Measurement - recorded_at = Date.today - # Insert random counts for the last 60 days - measurements = 60.times.flat_map do - recorded_at = (recorded_at - 1.day).end_of_day - 5.minutes + measurements = model_class.identifiers.flat_map do |_, id| + recorded_at = 60.days.ago + current_count = rand(1_000_000) + + # Insert random counts for the last 60 days + Array.new(60) do + recorded_at = (recorded_at + 1.day).end_of_day - 5.minutes + + # Normally our counts should slowly increase as the gitlab instance grows. + # Small chance (10%) to have a slight decrease (simulating cleanups, bulk delete) + if rand < chance_for_decrement + current_count -= rand(max_decrease) + else + current_count += rand(max_increase) + end - model_class.identifiers.map do |_, id| { recorded_at: recorded_at, - count: rand(1_000_000), + count: current_count, identifier: id } end diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 6a6e7bde1b2..ca36f729c25 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -104,12 +104,12 @@ The following table lists available parameters for jobs: | [`script`](#script) | Shell script that is executed by a runner. | | [`after_script`](#before_script-and-after_script) | Override a set of commands that are executed after job. | | [`allow_failure`](#allow_failure) | Allow job to fail. Failed job does not contribute to commit status. | -| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`. | +| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, and `artifacts:reports`. | | [`before_script`](#before_script-and-after_script) | Override a set of commands that are executed before job. | -| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. | +| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, `cache:when`, and `cache:policy`. | | [`coverage`](#coverage) | Code coverage settings for a given job. | | [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. | -| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. | +| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in`, and `environment:action`. | | [`except`](#onlyexcept-basic) | Limit when jobs are not created. Also available: [`except:refs`, `except:kubernetes`, `except:variables`, and `except:changes`](#onlyexcept-advanced). | | [`extends`](#extends) | Configuration entries that this job inherits from. | | [`image`](#image) | Use Docker images. Also available: `image:name` and `image:entrypoint`. | @@ -2914,6 +2914,28 @@ rspec: - binaries/ ``` +#### `cache:when` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18969) in GitLab 13.5 and GitLab Runner v13.5.0. + +`cache:when` defines when to save the cache, based on the status of the job. You can +set `cache:when` to: + +- `on_success` - save the cache only when the job succeeds. This is the default. +- `on_failure` - save the cache only when the job fails. +- `always` - save the cache regardless of the job status. + +For example, to store a cache whether or not the job fails or succeeds: + +```yaml +rspec: + script: rspec + cache: + paths: + - rspec/ + when: 'always' +``` + #### `cache:policy` > Introduced in GitLab 9.4. @@ -3236,7 +3258,7 @@ failure. 1. `on_failure` - upload artifacts only when the job fails. 1. `always` - upload artifacts regardless of the job status. -To upload artifacts only when job fails: +For example, to upload artifacts only when a job fails: ```yaml job: diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index ef23b6c4ed2..f909866d44e 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -76,6 +76,10 @@ How we use SVG for our [Icons and Illustrations](icons.md). General information about frontend [dependencies](dependencies.md) and how we manage them. +## Keyboard Shortcuts + +How we implement [keyboard shortcuts](keyboard_shortcuts.md) that can be customized and disabled. + ## Frontend FAQ Read the [frontend's FAQ](frontend_faq.md) for common small pieces of helpful information. diff --git a/doc/development/fe_guide/keyboard_shortcuts.md b/doc/development/fe_guide/keyboard_shortcuts.md new file mode 100644 index 00000000000..da9b3702a8d --- /dev/null +++ b/doc/development/fe_guide/keyboard_shortcuts.md @@ -0,0 +1,98 @@ +# Implementing keyboard shortcuts + +We use [Mousetrap](https://craig.is/killing/mice) to implement keyboard +shortcuts in GitLab. + +Mousetrap provides an API that allows keyboard shortcut strings (like +`mod+shift+p` or `p b`) to be bound to a JavaScript handler: + +```javascript +// Don't do this; see note below +Mousetrap.bind('p b', togglePerformanceBar) +``` + +However, associating a hard-coded key sequence to a handler (as shown above) +prevents these keyboard shortcuts from being customized or disabled by users. + +To allow keyboard shortcuts to be customized, commands are defined in +`~/behaviors/shortcuts/keybindings.js`. The `keysFor` method is responsible for +returning the correct key sequence for the provided command: + +```javascript +import { keysFor, TOGGLE_PERFORMANCE_BAR } from '~/behaviors/shortcuts/keybindings' + +Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), togglePerformanceBar); +``` + +## Shortcut customization + +`keybindings.js` stores keyboard shortcut customizations as a JSON string in +`localStorage`. When `keybindings.js` is first imported, it fetches any +customizations from `localStorage` and merges these customizations into the +default set of keybindings. There is no UI to edit these customizations. + +## Adding new shortcuts + +Because keyboard shortcuts can be customized or disabled by end users, +developers are encouraged to build _lots_ of keyboard shortcuts into GitLab. +Shortcuts that are less likely to be used should be +[disabled](#disabling-shortcuts) by default. + +To add a new shortcut, define and export a new command string in +`keybindings.js`: + +```javascript +export const MAKE_COFFEE = 'foodAndBeverage.makeCoffee'; +``` + +Next, add a new command definition under the appropriate group in the +`keybindingGroups` array: + +```javascript +{ + description: s__('KeyboardShortcuts|Make coffee'), + command: MAKE_COFFEE, + defaultKeys: ['mod+shift+c'], + customKeys: customizations[MAKE_COFFEE], +} +``` + +Finally, in the application code, import the `keysFor` function and the new +command and bind the shortcut to the handler using Mousetrap: + +```javascript +import { keysFor, MAKE_COFFEE } from '~/behaviors/shortcuts/keybindings' + +Mousetrap.bind(keysFor(MAKE_COFFEE), makeCoffee); +``` + +See the existing the command definitions in `keybindings.js` for more examples. + +## Disabling shortcuts + +A shortcut can be disabled, also known as _unassigned_, by assigning the +shortcut to an empty array `[]`. For example, to introduce a new shortcut that +is disabled by default, a command can be defined like this: + +```javascript +export const MAKE_MOCHA = 'foodAndBeverage.makeMocha'; + +{ + description: s__('KeyboardShortcuts|Make a mocha'), + command: MAKE_MOCHA, + defaultKeys: [], + customKeys: customizations[MAKE_MOCHA], +} +``` + +## Make cross-platform shortcuts + +It's difficult to make shortcuts that work well in all platforms and browsers. +This is one of the reasons that being able to customize and disable shortcuts is +so important. + +One important way to make keyboard shortcuts more portable is to use the `mod` +shortcut string, which resolves to `command` on Mac and `ctrl` otherwise. + +See [Mousetrap's documentation](https://craig.is/killing/mice#api.bind.combo) +for more information. diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 1105f419c8b..8cb20ed4829 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -1419,26 +1419,20 @@ Example: ```markdown | header 1 | header 2 | header 3 | -| --- | ------ |---------:| +| --- | ------ |----------| | cell 1 | cell 2 | cell 3 | | cell 4 | cell 5 is longer | cell 6 is much longer than the others, but that's ok. It eventually wraps the text when the cell is too large for the display size. | -| cell 7 | | cell
9 | -| cell 10 |
  • - [ ] Task One
|
  • - [ ] Task Two
  • - [ ] Task Three
| +| cell 7 | | cell 9 | ``` | header 1 | header 2 | header 3 | -| --- | ------ |---------:| +| --- | ------ |----------| | cell 1 | cell 2 | cell 3 | | cell 4 | cell 5 is longer | cell 6 is much longer than the others, but that's ok. It eventually wraps the text when the cell is too large for the display size. | -| cell 7 | | cell
9 | -| cell 10 |
  • - [ ] Task One
|
  • - [ ] Task Two
  • - [ ] Task Three
| +| cell 7 | | cell 9 | Additionally, you can choose the alignment of text within columns by adding colons (`:`) -to the sides of the "dash" lines in the second row. This affects every cell in the column. - -NOTE: **Note:** -[Within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables), -the headers are always left-aligned in Chrome and Firefox, and centered in Safari. +to the sides of the "dash" lines in the second row. This affects every cell in the column: ```markdown | Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | @@ -1452,6 +1446,34 @@ the headers are always left-aligned in Chrome and Firefox, and centered in Safar | Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | | Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | +[Within GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#tables), +the headers are always left-aligned in Chrome and Firefox, and centered in Safari. + +You can use HTML formatting to adjust the rendering of tables. For example, you can +use `
` tags to force a cell to have multiple lines: + +```markdown +| Name | Details | +|------|---------| +| Item1 | This is on one line | +| Item2 | This item has:
- Multiple items
- That we want listed separately | +``` + +| Name | Details | +|------|---------| +| Item1 | This is on one line | +| Item2 | This item has:
- Multiple items
- That we want listed separately | + +You can use HTML formatting within GitLab itself to add [task lists](#task-lists) with checkboxes, +but they do not render properly on `docs.gitlab.com`: + +```markdown +| header 1 | header 2 | +|----------|----------| +| cell 1 | cell 2 | +| cell 3 |
  • - [ ] Task one
  • - [ ] Task two
| +``` + #### Copy from spreadsheet and paste in Markdown [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27205) in GitLab 12.7. diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index c003eec784b..33235eef7b8 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -469,6 +469,20 @@ Cleanup policies can be run on all projects, with these exceptions: There are performance risks with enabling it for all projects, especially if you are using an [external registry](./index.md#use-with-external-container-registries). +- For self-managed GitLab instances, you can enable or disable the cleanup policy for a specific + project. + + To enable it: + + ```ruby + Feature.enable(:container_expiration_policies_historic_entry, Project.find()) + ``` + + To disable it: + + ```ruby + Feature.disable(:container_expiration_policies_historic_entry, Project.find()) + ``` ### How the cleanup policy works diff --git a/lib/api/entities/job_request/cache.rb b/lib/api/entities/job_request/cache.rb index a75affbaf84..cd533d7e5b3 100644 --- a/lib/api/entities/job_request/cache.rb +++ b/lib/api/entities/job_request/cache.rb @@ -4,7 +4,7 @@ module API module Entities module JobRequest class Cache < Grape::Entity - expose :key, :untracked, :paths, :policy + expose :key, :untracked, :paths, :policy, :when end end end diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb index a304d9b724f..6b036182706 100644 --- a/lib/gitlab/ci/config/entry/cache.rb +++ b/lib/gitlab/ci/config/entry/cache.rb @@ -9,14 +9,28 @@ module Gitlab # class Cache < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[key untracked paths policy].freeze + ALLOWED_KEYS = %i[key untracked paths when policy].freeze + ALLOWED_POLICY = %w[pull-push push pull].freeze DEFAULT_POLICY = 'pull-push' + ALLOWED_WHEN = %w[on_success on_failure always].freeze + DEFAULT_WHEN = 'on_success' validations do - validates :config, allowed_keys: ALLOWED_KEYS - validates :policy, inclusion: { in: %w[pull-push push pull], message: 'should be pull-push, push, or pull' }, allow_blank: true + validates :config, type: Hash, allowed_keys: ALLOWED_KEYS + validates :policy, + inclusion: { in: ALLOWED_POLICY, message: 'should be pull-push, push, or pull' }, + allow_blank: true + + with_options allow_nil: true do + validates :when, + inclusion: { + in: ALLOWED_WHEN, + message: 'should be on_success, on_failure or always' + } + end end entry :key, Entry::Key, @@ -28,13 +42,15 @@ module Gitlab entry :paths, Entry::Paths, description: 'Specify which paths should be cached across builds.' - attributes :policy + attributes :policy, :when def value result = super result[:key] = key_value result[:policy] = policy || DEFAULT_POLICY + # Use self.when to avoid conflict with reserved word + result[:when] = self.when || DEFAULT_WHEN result end diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb index d7ba8624882..66cd57b8cf3 100644 --- a/lib/gitlab/ci/config/entry/needs.rb +++ b/lib/gitlab/ci/config/entry/needs.rb @@ -7,7 +7,7 @@ module Gitlab ## # Entry that represents a set of needs dependencies. # - class Needs < ::Gitlab::Config::Entry::Node + class Needs < ::Gitlab::Config::Entry::ComposableArray include ::Gitlab::Config::Entry::Validatable validations do @@ -29,27 +29,16 @@ module Gitlab end end - def compose!(deps = nil) - super(deps) do - [@config].flatten.each_with_index do |need, index| - @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need) - .value(need) - .with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord - .create! - end - - @entries.each_value do |entry| - entry.compose!(deps) - end - end - end - def value - values = @entries.values.select(&:type) + values = @entries.select(&:type) values.group_by(&:type).transform_values do |values| values.map(&:value) end end + + def composable_class + Entry::Need + end end end end diff --git a/lib/gitlab/ci/config/entry/ports.rb b/lib/gitlab/ci/config/entry/ports.rb index 01ffcc7dd87..d26b31deca8 100644 --- a/lib/gitlab/ci/config/entry/ports.rb +++ b/lib/gitlab/ci/config/entry/ports.rb @@ -7,7 +7,7 @@ module Gitlab ## # Entry that represents a configuration of the ports of a Docker service. # - class Ports < ::Gitlab::Config::Entry::Node + class Ports < ::Gitlab::Config::Entry::ComposableArray include ::Gitlab::Config::Entry::Validatable validations do @@ -16,28 +16,8 @@ module Gitlab validates :config, port_unique: true end - def compose!(deps = nil) - super do - @entries = [] - @config.each do |config| - @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Port) - .value(config || {}) - .with(key: "port", parent: self, description: "port definition.") # rubocop:disable CodeReuse/ActiveRecord - .create! - end - - @entries.each do |entry| - entry.compose!(deps) - end - end - end - - def value - @entries.map(&:value) - end - - def descendants - @entries + def composable_class + Entry::Port end end end diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb index 2fbc3d9e367..bf74f995e80 100644 --- a/lib/gitlab/ci/config/entry/rules.rb +++ b/lib/gitlab/ci/config/entry/rules.rb @@ -4,7 +4,7 @@ module Gitlab module Ci class Config module Entry - class Rules < ::Gitlab::Config::Entry::Node + class Rules < ::Gitlab::Config::Entry::ComposableArray include ::Gitlab::Config::Entry::Validatable validations do @@ -12,24 +12,13 @@ module Gitlab validates :config, type: Array end - def compose!(deps = nil) - super(deps) do - @config.each_with_index do |rule, index| - @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule) - .value(rule) - .with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord - .create! - end - - @entries.each_value do |entry| - entry.compose!(deps) - end - end - end - def value @config end + + def composable_class + Entry::Rules::Rule + end end end end diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index 83baa83711f..44e2903a300 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -7,7 +7,7 @@ module Gitlab ## # Entry that represents a configuration of Docker services. # - class Services < ::Gitlab::Config::Entry::Node + class Services < ::Gitlab::Config::Entry::ComposableArray include ::Gitlab::Config::Entry::Validatable validations do @@ -15,28 +15,8 @@ module Gitlab validates :config, services_with_ports_alias_unique: true, if: ->(record) { record.opt(:with_image_ports) } end - def compose!(deps = nil) - super do - @entries = [] - @config.each do |config| - @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service) - .value(config || {}) - .with(key: "service", parent: self, description: "service definition.") # rubocop:disable CodeReuse/ActiveRecord - .create! - end - - @entries.each do |entry| - entry.compose!(deps) - end - end - end - - def value - @entries.map(&:value) - end - - def descendants - @entries + def composable_class + Entry::Service end end end diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb index a4127ea0be2..8d6fe13c3b9 100644 --- a/lib/gitlab/ci/pipeline/seed/build/cache.rb +++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb @@ -13,6 +13,7 @@ module Gitlab @paths = local_cache.delete(:paths) @policy = local_cache.delete(:policy) @untracked = local_cache.delete(:untracked) + @when = local_cache.delete(:when) raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any? end @@ -24,7 +25,8 @@ module Gitlab key: key_string, paths: @paths, policy: @policy, - untracked: @untracked + untracked: @untracked, + when: @when }.compact.presence }.compact } diff --git a/lib/gitlab/config/entry/composable_array.rb b/lib/gitlab/config/entry/composable_array.rb new file mode 100644 index 00000000000..e7ad259e826 --- /dev/null +++ b/lib/gitlab/config/entry/composable_array.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Gitlab + module Config + module Entry + ## + # Entry that represents a composable array definition + # + class ComposableArray < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include Gitlab::Utils::StrongMemoize + + # TODO: Refactor `Validatable` code so that validations can apply to a child class + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231 + validations do + validates :config, type: Array + end + + def compose!(deps = nil) + super do + @entries = Array(@entries) + + # TODO: Isolate handling for a hash via: `[@config].flatten` to the `Needs` entry + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/264376 + [@config].flatten.each_with_index do |value, index| + raise ArgumentError, 'Missing Composable class' unless composable_class + + composable_class_name = composable_class.name.demodulize.underscore + + @entries << ::Gitlab::Config::Entry::Factory.new(composable_class) + .value(value) + .with(key: composable_class_name, parent: self, description: "#{composable_class_name} definition") # rubocop:disable CodeReuse/ActiveRecord + .create! + end + + @entries.each do |entry| + entry.compose!(deps) + end + end + end + + def value + @entries.map(&:value) + end + + def descendants + @entries + end + + def composable_class + strong_memoize(:composable_class) do + opt(:composable_class) + end + end + end + end + end +end diff --git a/lib/gitlab/config/entry/composable_hash.rb b/lib/gitlab/config/entry/composable_hash.rb index 74070915940..9531b7e56fd 100644 --- a/lib/gitlab/config/entry/composable_hash.rb +++ b/lib/gitlab/config/entry/composable_hash.rb @@ -10,7 +10,7 @@ module Gitlab class ComposableHash < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable - # TODO: Refactor Validatable so these validations will not apply to a child class + # TODO: Refactor `Validatable` code so that validations can apply to a child class # See: https://gitlab.com/gitlab-org/gitlab/-/issues/263231 validations do validates :config, type: Hash diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb index 697ced3590b..a205861b9a9 100644 --- a/lib/system_check/app/redis_version_check.rb +++ b/lib/system_check/app/redis_version_check.rb @@ -37,7 +37,7 @@ module SystemCheck @custom_error_message ) for_more_information( - 'doc/administration/high_availability/redis.md#provide-your-own-redis-instance' + 'doc/administration/redis/index.html#redis-replication-and-failover-using-the-non-bundled-redis' ) fix_and_rerun end diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index 1e28d15f75e..e2c92054d62 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -38,7 +38,7 @@ namespace :gettext do Rake::Task['gettext:find'].invoke # leave only the required changes. - unless system(*%w(git checkout -- locale/*/gitlab.po)) + unless system(*%w(git -c core.hooksPath=/dev/null checkout -- locale/*/gitlab.po)) raise 'failed to cleanup generated locale/*/gitlab.po files' end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8d38319762e..3a790d87539 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14773,6 +14773,12 @@ msgstr "" msgid "KeyboardKey|Ctrl+" msgstr "" +msgid "KeyboardShortcuts|Global Shortcuts" +msgstr "" + +msgid "KeyboardShortcuts|Toggle the Performance Bar" +msgstr "" + msgid "Keys" msgstr "" diff --git a/qa/spec/fixtures/banana_sample.gif b/qa/qa/fixtures/designs/banana_sample.gif similarity index 100% rename from qa/spec/fixtures/banana_sample.gif rename to qa/qa/fixtures/designs/banana_sample.gif diff --git a/qa/spec/fixtures/tanuki.jpg b/qa/qa/fixtures/designs/tanuki.jpg similarity index 100% rename from qa/spec/fixtures/tanuki.jpg rename to qa/qa/fixtures/designs/tanuki.jpg diff --git a/qa/qa/fixtures/designs/update/tanuki.jpg b/qa/qa/fixtures/designs/update/tanuki.jpg new file mode 100644 index 0000000000000000000000000000000000000000..162beda6c7bbb35c2c4e1d78087bc800f79d0f18 GIT binary patch literal 83907 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 Settings > CI/CD > Container registry tag expiration policy', :js do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace, container_registry_enabled: container_registry_enabled) } + using RSpec::Parameterized::TableSyntax + + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) } + let(:container_registry_enabled) { true } + let(:container_registry_enabled_on_project) { true } + + subject { visit project_settings_ci_cd_path(project) } before do + project.update!(container_registry_enabled: container_registry_enabled_on_project) + sign_in(user) - stub_container_registry_config(enabled: true) + stub_container_registry_config(enabled: container_registry_enabled) stub_feature_flags(new_variables_ui: false) end context 'as owner' do - before do - visit project_settings_ci_cd_path(project) - end - it 'shows available section' do + subject + settings_block = find('#js-registry-policies') expect(settings_block).to have_text 'Cleanup policy for tags' end it 'saves cleanup policy submit the form' do + subject + within '#js-registry-policies' do within '.card-body' do select('7 days until tags are automatically removed', from: 'Expiration interval:') @@ -40,6 +48,8 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p end it 'does not save cleanup policy submit form with invalid regex' do + subject + within '#js-registry-policies' do within '.card-body' do fill_in('Tags with names matching this regex pattern will expire:', with: '*-production') @@ -53,25 +63,53 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p end end - context 'when registry is disabled' do - before do - stub_container_registry_config(enabled: false) - visit project_settings_ci_cd_path(project) + context 'with a project without expiration policy' do + where(:application_setting, :feature_flag, :result) do + true | true | :available_section + true | false | :available_section + false | true | :available_section + false | false | :disabled_message end + with_them do + before do + project.container_expiration_policy.destroy! + stub_feature_flags(container_expiration_policies_historic_entry: false) + stub_application_setting(container_expiration_policies_enable_historic_entries: application_setting) + stub_feature_flags(container_expiration_policies_historic_entry: project) if feature_flag + end + + it 'displays the expected result' do + subject + + within '#js-registry-policies' do + case result + when :available_section + expect(find('.card-header')).to have_content('Tag expiration policy') + when :disabled_message + expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled') + end + end + end + end + end + + context 'when registry is disabled' do + let(:container_registry_enabled) { false } + it 'does not exists' do + subject + expect(page).not_to have_selector('#js-registry-policies') end end context 'when container registry is disabled on project' do - let(:container_registry_enabled) { false } - - before do - visit project_settings_ci_cd_path(project) - end + let(:container_registry_enabled_on_project) { false } it 'does not exists' do + subject + expect(page).not_to have_selector('#js-registry-policies') end end diff --git a/spec/frontend/behaviors/shortcuts/keybindings_spec.js b/spec/frontend/behaviors/shortcuts/keybindings_spec.js new file mode 100644 index 00000000000..23fea79f828 --- /dev/null +++ b/spec/frontend/behaviors/shortcuts/keybindings_spec.js @@ -0,0 +1,66 @@ +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; + +describe('~/behaviors/shortcuts/keybindings.js', () => { + let keysFor; + let TOGGLE_PERFORMANCE_BAR; + let LOCAL_STORAGE_KEY; + + beforeAll(() => { + useLocalStorageSpy(); + }); + + const setupCustomizations = async customizationsAsString => { + localStorage.clear(); + + if (customizationsAsString) { + localStorage.setItem(LOCAL_STORAGE_KEY, customizationsAsString); + } + + jest.resetModules(); + ({ keysFor, TOGGLE_PERFORMANCE_BAR, LOCAL_STORAGE_KEY } = await import( + '~/behaviors/shortcuts/keybindings' + )); + }; + + describe('when a command has not been customized', () => { + beforeEach(async () => { + await setupCustomizations('{}'); + }); + + it('returns the default keybinding for the command', () => { + expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']); + }); + }); + + describe('when a command has been customized', () => { + const customization = ['p b a r']; + + beforeEach(async () => { + await setupCustomizations(JSON.stringify({ [TOGGLE_PERFORMANCE_BAR]: customization })); + }); + + it('returns the default keybinding for the command', () => { + expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(customization); + }); + }); + + describe("when the localStorage entry isn't valid JSON", () => { + beforeEach(async () => { + await setupCustomizations('{'); + }); + + it('returns the default keybinding for the command', () => { + expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']); + }); + }); + + describe(`when localStorage doesn't contain the ${LOCAL_STORAGE_KEY} key`, () => { + beforeEach(async () => { + await setupCustomizations(); + }); + + it('returns the default keybinding for the command', () => { + expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']); + }); + }); +}); diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js index 951b3f6258b..c65a39b9083 100644 --- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js +++ b/spec/frontend/diffs/components/inline_diff_table_row_spec.js @@ -1,5 +1,4 @@ import { shallowMount } from '@vue/test-utils'; -import { TEST_HOST } from 'helpers/test_constants'; import { createStore } from '~/mr_notes/stores'; import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; @@ -28,13 +27,6 @@ describe('InlineDiffTableRow', () => { }); }; - const setWindowLocation = value => { - Object.defineProperty(window, 'location', { - writable: true, - value, - }); - }; - beforeEach(() => { store = createStore(); store.state.notes.userData = TEST_USER; @@ -122,22 +114,15 @@ describe('InlineDiffTableRow', () => { const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' }); it.each` - userData | query | mergeRefHeadComments | expectation - ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true} - ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true} - ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false} - ${null} | ${''} | ${true} | ${false} - `( - 'exists is $expectation - with userData ($userData) query ($query)', - ({ userData, query, mergeRefHeadComments, expectation }) => { - store.state.notes.userData = userData; - gon.features = { mergeRefHeadComments }; - setWindowLocation({ href: `${TEST_HOST}?${query}` }); - createComponent({}, store); + userData | expectation + ${TEST_USER} | ${true} + ${null} | ${false} + `('exists is $expectation - with userData ($userData)', ({ userData, expectation }) => { + store.state.notes.userData = userData; + createComponent({}, store); - expect(findNoteButton().exists()).toBe(expectation); - }, - ); + expect(findNoteButton().exists()).toBe(expectation); + }); it.each` isHover | line | expectation diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js index 13c4ce06f18..13031bd8b66 100644 --- a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js +++ b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import { shallowMount } from '@vue/test-utils'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import { TEST_HOST } from 'helpers/test_constants'; import { createStore } from '~/mr_notes/stores'; import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue'; import diffFileMockData from '../mock_data/diff_file'; @@ -186,13 +185,6 @@ describe('ParallelDiffTableRow', () => { }); }; - const setWindowLocation = value => { - Object.defineProperty(window, 'location', { - writable: true, - value, - }); - }; - beforeEach(() => { // eslint-disable-next-line prefer-destructuring thisLine = diffFileMockData.parallel_diff_lines[2]; @@ -228,19 +220,15 @@ describe('ParallelDiffTableRow', () => { const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButtonLeft' }); it.each` - hover | line | userData | query | mergeRefHeadComments | expectation - ${true} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true} - ${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false} - ${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true} - ${true} | ${{}} | ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false} - ${true} | ${{}} | ${null} | ${''} | ${true} | ${false} - ${false} | ${{}} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${false} + hover | line | userData | expectation + ${true} | ${{}} | ${TEST_USER} | ${true} + ${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${false} + ${true} | ${{}} | ${null} | ${false} + ${false} | ${{}} | ${TEST_USER} | ${false} `( - 'exists is $expectation - with userData ($userData) query ($query)', - async ({ hover, line, userData, query, mergeRefHeadComments, expectation }) => { + 'exists is $expectation - with userData ($userData)', + async ({ hover, line, userData, expectation }) => { store.state.notes.userData = userData; - gon.features = { mergeRefHeadComments }; - setWindowLocation({ href: `${TEST_HOST}?${query}` }); createComponent(line, store); if (hover) await wrapper.find('.line_holder').trigger('mouseover'); diff --git a/spec/frontend/issuable_show/components/issuable_header_spec.js b/spec/frontend/issuable_show/components/issuable_header_spec.js new file mode 100644 index 00000000000..fad8ec8a891 --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_header_spec.js @@ -0,0 +1,132 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlAvatarLabeled } from '@gitlab/ui'; + +import IssuableHeader from '~/issuable_show/components/issuable_header.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +const issuableHeaderProps = { + ...mockIssuable, + ...mockIssuableShowProps, +}; + +const createComponent = (propsData = issuableHeaderProps) => + shallowMount(IssuableHeader, { + propsData, + slots: { + 'status-badge': 'Open', + 'header-actions': ` + + New issuable + `, + }, + }); + +describe('IssuableHeader', () => { + let wrapper; + const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`); + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('computed', () => { + describe('authorId', () => { + it('returns numeric ID from GraphQL ID of `author` prop', () => { + expect(wrapper.vm.authorId).toBe(1); + }); + }); + }); + + describe('handleRightSidebarToggleClick', () => { + beforeEach(() => { + setFixtures(''); + }); + + it('dispatches `click` event on sidebar toggle button', () => { + wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button'); + jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn); + + wrapper.vm.handleRightSidebarToggleClick(); + + expect(wrapper.vm.toggleSidebarButtonEl.dispatchEvent).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'click', + }), + ); + }); + }); + + describe('template', () => { + it('renders issuable status icon and text', () => { + const statusBoxEl = findByTestId('status'); + + expect(statusBoxEl.exists()).toBe(true); + expect(statusBoxEl.find(GlIcon).props('name')).toBe(mockIssuableShowProps.statusIcon); + expect(statusBoxEl.text()).toContain('Open'); + }); + + it('renders blocked icon when issuable is blocked', async () => { + wrapper.setProps({ + blocked: true, + }); + + await wrapper.vm.$nextTick(); + + const blockedEl = findByTestId('blocked'); + + expect(blockedEl.exists()).toBe(true); + expect(blockedEl.find(GlIcon).props('name')).toBe('lock'); + }); + + it('renders confidential icon when issuable is confidential', async () => { + wrapper.setProps({ + confidential: true, + }); + + await wrapper.vm.$nextTick(); + + const confidentialEl = findByTestId('confidential'); + + expect(confidentialEl.exists()).toBe(true); + expect(confidentialEl.find(GlIcon).props('name')).toBe('eye-slash'); + }); + + it('renders issuable author avatar', () => { + const { username, name, webUrl, avatarUrl } = mockIssuable.author; + const avatarElAttrs = { + 'data-user-id': '1', + 'data-username': username, + 'data-name': name, + href: webUrl, + target: '_blank', + }; + const avatarEl = findByTestId('avatar'); + expect(avatarEl.exists()).toBe(true); + expect(avatarEl.attributes()).toMatchObject(avatarElAttrs); + expect(avatarEl.find(GlAvatarLabeled).attributes()).toMatchObject({ + size: '24', + src: avatarUrl, + label: name, + }); + }); + + it('renders sidebar toggle button', () => { + const toggleButtonEl = findByTestId('sidebar-toggle'); + + expect(toggleButtonEl.exists()).toBe(true); + expect(toggleButtonEl.props('icon')).toBe('chevron-double-lg-left'); + }); + + it('renders header actions', () => { + const actionsEl = findByTestId('header-actions'); + + expect(actionsEl.find('button.js-close').exists()).toBe(true); + expect(actionsEl.find('a.js-new').exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/issuable_show/mock_data.js b/spec/frontend/issuable_show/mock_data.js new file mode 100644 index 00000000000..0a4c0880856 --- /dev/null +++ b/spec/frontend/issuable_show/mock_data.js @@ -0,0 +1,33 @@ +import { mockIssuable as issuable } from '../issuable_list/mock_data'; + +export const mockIssuable = { + ...issuable, + id: 'gid://gitlab/Issue/30', + title: 'Sample title', + titleHtml: 'Sample title', + description: '# Summary', + descriptionHtml: + '

Summary

', + state: 'opened', + blocked: false, + confidential: false, + currentUserTodos: { + nodes: [ + { + id: 'gid://gitlab/Todo/489', + state: 'done', + }, + ], + }, +}; + +export const mockIssuableShowProps = { + issuable: mockIssuable, + descriptionHelpPath: '/help/user/markdown', + descriptionPreviewPath: '/gitlab-org/gitlab-shell/preview_markdown', + editFormVisible: false, + enableAutocomplete: true, + enableEdit: true, + statusBadgeClass: 'status-box-open', + statusIcon: 'issue-open-m', +}; diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index a7973d66b50..d168de5bf8b 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -69,6 +69,34 @@ describe('Date time utils', () => { }); }); + describe('formatDateAsMonth', () => { + it('should format dash cased date properly', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth(new Date('2020-06-28')); + + expect(formattedMonth).toBe('Jun'); + }); + + it('should format return the non-abbreviated month', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth(new Date('2020-07-28'), { + abbreviated: false, + }); + + expect(formattedMonth).toBe('July'); + }); + + it('should format date with slashes properly', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth(new Date('07/23/2016')); + + expect(formattedMonth).toBe('Jul'); + }); + + it('should format ISO date properly', () => { + const formattedMonth = datetimeUtility.formatDateAsMonth('2016-07-23T00:00:00.559Z'); + + expect(formattedMonth).toBe('Jul'); + }); + }); + describe('formatDate', () => { it('should format date properly', () => { const formattedDate = datetimeUtility.formatDate(new Date('07/23/2016')); diff --git a/spec/helpers/container_expiration_policies_helper_spec.rb b/spec/helpers/container_expiration_policies_helper_spec.rb index b2a03f8d90f..7ad3804e3a9 100644 --- a/spec/helpers/container_expiration_policies_helper_spec.rb +++ b/spec/helpers/container_expiration_policies_helper_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe ContainerExpirationPoliciesHelper do + using RSpec::Parameterized::TableSyntax + describe '#keep_n_options' do it 'returns keep_n options formatted for dropdown usage' do expected_result = [ @@ -44,4 +46,27 @@ RSpec.describe ContainerExpirationPoliciesHelper do expect(helper.older_than_options).to eq(expected_result) end end + + describe '#container_expiration_policies_historic_entry_enabled?' do + let_it_be(:project) { build_stubbed(:project) } + + subject { helper.container_expiration_policies_historic_entry_enabled?(project) } + + where(:application_setting, :feature_flag, :expected_result) do + true | true | true + true | false | true + false | true | true + false | false | false + end + + with_them do + before do + stub_feature_flags(container_expiration_policies_historic_entry: false) + stub_application_setting(container_expiration_policies_enable_historic_entries: application_setting) + stub_feature_flags(container_expiration_policies_historic_entry: project) if feature_flag + end + + it { is_expected.to eq(expected_result) } + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 3501812b76e..80427eaa6ee 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -13,18 +13,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do context 'when entry config value is correct' do let(:policy) { nil } let(:key) { 'some key' } + let(:when_config) { nil } let(:config) do - { key: key, + { + key: key, untracked: true, - paths: ['some/path/'], - policy: policy } + paths: ['some/path/'] + }.tap do |config| + config[:policy] = policy if policy + config[:when] = when_config if when_config + end end describe '#value' do shared_examples 'hash key value' do it 'returns hash value' do - expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push') + expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success') end end @@ -49,6 +54,48 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do expect(entry.value).to match(a_hash_including(key: nil)) end end + + context 'with `policy`' do + using RSpec::Parameterized::TableSyntax + + where(:policy, :result) do + 'pull-push' | 'pull-push' + 'push' | 'push' + 'pull' | 'pull' + 'unknown' | 'unknown' # invalid + end + + with_them do + it { expect(entry.value).to include(policy: result) } + end + end + + context 'without `policy`' do + it 'assigns policy to default' do + expect(entry.value).to include(policy: 'pull-push') + end + end + + context 'with `when`' do + using RSpec::Parameterized::TableSyntax + + where(:when_config, :result) do + 'on_success' | 'on_success' + 'on_failure' | 'on_failure' + 'always' | 'always' + 'unknown' | 'unknown' # invalid + end + + with_them do + it { expect(entry.value).to include(when: result) } + end + end + + context 'without `when`' do + it 'assigns when to default' do + expect(entry.value).to include(when: 'on_success') + end + end end describe '#valid?' do @@ -61,28 +108,41 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end end - context 'policy is pull-push' do - let(:policy) { 'pull-push' } + context 'with `policy`' do + using RSpec::Parameterized::TableSyntax - it { is_expected.to be_valid } - it { expect(entry.value).to include(policy: 'pull-push') } + where(:policy, :valid) do + 'pull-push' | true + 'push' | true + 'pull' | true + 'unknown' | false + end + + with_them do + it 'returns expected validity' do + expect(entry.valid?).to eq(valid) + end + end end - context 'policy is push' do - let(:policy) { 'push' } + context 'with `when`' do + using RSpec::Parameterized::TableSyntax - it { is_expected.to be_valid } - it { expect(entry.value).to include(policy: 'push') } + where(:when_config, :valid) do + 'on_success' | true + 'on_failure' | true + 'always' | true + 'unknown' | false + end + + with_them do + it 'returns expected validity' do + expect(entry.valid?).to eq(valid) + end + end end - context 'policy is pull' do - let(:policy) { 'pull' } - - it { is_expected.to be_valid } - it { expect(entry.value).to include(policy: 'pull') } - end - - context 'when key is missing' do + context 'with key missing' do let(:config) do { untracked: true, paths: ['some/path/'] } @@ -110,13 +170,21 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'when policy is unknown' do - let(:config) { { policy: "unknown" } } + let(:config) { { policy: 'unknown' } } it 'reports error' do is_expected.to include('cache policy should be pull-push, push, or pull') end end + context 'when `when` is unknown' do + let(:config) { { when: 'unknown' } } + + it 'reports error' do + is_expected.to include('cache when should be on_success, on_failure or always') + end + end + context 'when descendants are invalid' do context 'with invalid keys' do let(:config) { { key: 1 } } diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index ab760b107f8..e0e8bc93770 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -537,7 +537,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'overrides default config' do expect(entry[:image].value).to eq(name: 'some_image') - expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push') + expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') end end @@ -552,7 +552,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'uses config from default entry' do expect(entry[:image].value).to eq 'specified' - expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push') + expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') end end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 252bda6461d..79716df6b60 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -127,7 +127,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root' }, ignore: false, after_script: ['make clean'], @@ -141,7 +141,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push' }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root' }, ignore: false, after_script: ['make clean'], @@ -156,7 +156,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, image: { name: "ruby:2.7" }, services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], - cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push" }, + cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }, only: { refs: %w(branches tags) }, variables: { 'VAR' => 'job' }, after_script: [], @@ -203,7 +203,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'root' }, ignore: false, after_script: ['make clean'], @@ -215,7 +215,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'ruby:2.7' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: "pull-push" }, + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, variables: { 'VAR' => 'job' }, ignore: false, after_script: ['make clean'], @@ -261,7 +261,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do describe '#cache_value' do it 'returns correct cache definition' do - expect(root.cache_value).to eq(key: 'a', policy: 'pull-push') + expect(root.cache_value).to eq(key: 'a', policy: 'pull-push', when: 'on_success') end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb index 74c014b6408..570706bfaac 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb @@ -224,7 +224,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do key: 'a-key', paths: ['vendor/ruby'], untracked: true, - policy: 'push' + policy: 'push', + when: 'on_success' } end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 31ccbdcd3c8..03579d0936c 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1361,7 +1361,8 @@ module Gitlab paths: ["logs/", "binaries/"], untracked: true, key: 'key', - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1383,7 +1384,8 @@ module Gitlab paths: ["logs/", "binaries/"], untracked: true, key: { files: ['file'] }, - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1402,7 +1404,8 @@ module Gitlab paths: ['logs/', 'binaries/'], untracked: true, key: 'key', - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1425,7 +1428,8 @@ module Gitlab paths: ['logs/', 'binaries/'], untracked: true, key: { files: ['file'] }, - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1448,7 +1452,8 @@ module Gitlab paths: ['logs/', 'binaries/'], untracked: true, key: { files: ['file'], prefix: 'prefix' }, - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end @@ -1468,7 +1473,8 @@ module Gitlab paths: ["test/"], untracked: false, key: 'local', - policy: 'pull-push' + policy: 'pull-push', + when: 'on_success' ) end end diff --git a/spec/lib/gitlab/config/entry/composable_array_spec.rb b/spec/lib/gitlab/config/entry/composable_array_spec.rb new file mode 100644 index 00000000000..77766cb3b0a --- /dev/null +++ b/spec/lib/gitlab/config/entry/composable_array_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Config::Entry::ComposableArray, :aggregate_failures do + let(:valid_config) do + [ + { + DATABASE_SECRET: 'passw0rd' + }, + { + API_TOKEN: 'passw0rd2' + } + ] + end + + let(:config) { valid_config } + let(:entry) { described_class.new(config) } + + before do + allow(entry).to receive(:composable_class).and_return(Gitlab::Config::Entry::Node) + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + + context 'is invalid' do + let(:config) { { hello: :world } } + + it { expect(entry).not_to be_valid } + end + end + + describe '#compose!' do + before do + entry.compose! + end + + it 'composes child entry with configured value' do + expect(entry.value).to eq(config) + end + + it 'composes child entries with configured values' do + expect(entry[0]).to be_a(Gitlab::Config::Entry::Node) + expect(entry[0].description).to eq('node definition') + expect(entry[0].key).to eq('node') + expect(entry[0].metadata).to eq({}) + expect(entry[0].parent.class).to eq(Gitlab::Config::Entry::ComposableArray) + expect(entry[0].value).to eq(DATABASE_SECRET: 'passw0rd') + expect(entry[1]).to be_a(Gitlab::Config::Entry::Node) + expect(entry[1].description).to eq('node definition') + expect(entry[1].key).to eq('node') + expect(entry[1].metadata).to eq({}) + expect(entry[1].parent.class).to eq(Gitlab::Config::Entry::ComposableArray) + expect(entry[1].value).to eq(API_TOKEN: 'passw0rd2') + end + + describe '#descendants' do + it 'creates descendant nodes' do + expect(entry.descendants.first).to be_a(Gitlab::Config::Entry::Node) + expect(entry.descendants.first.value).to eq(DATABASE_SECRET: 'passw0rd') + expect(entry.descendants.second).to be_a(Gitlab::Config::Entry::Node) + expect(entry.descendants.second.value).to eq(API_TOKEN: 'passw0rd2') + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index ab22a203d06..f1d51324bbf 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2554,94 +2554,6 @@ RSpec.describe Ci::Build do end end - describe 'CHANGED_PAGES variables' do - let(:route_map_yaml) do - <<~ROUTEMAP - - source: 'bar/branch-test.txt' - public: '/bar/branches' - - source: 'with space/README.md' - public: '/README' - ROUTEMAP - end - - before do - allow_any_instance_of(Project) - .to receive(:route_map_for).with(/.+/) - .and_return(Gitlab::RouteMap.new(route_map_yaml)) - end - - context 'with a deployment environment and a merge request' do - let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let(:environment) { create(:environment, project: merge_request.project, name: "foo-#{project.default_branch}") } - let(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) } - - let(:full_urls) do - [ - File.join(environment.external_url, '/bar/branches'), - File.join(environment.external_url, '/README') - ] - end - - it 'populates CI_MERGE_REQUEST_CHANGED_PAGES_* variables' do - expect(subject).to include( - { - key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', - value: '/bar/branches,/README', - public: true, - masked: false - }, - { - key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', - value: full_urls.join(','), - public: true, - masked: false - } - ) - end - - context 'with a deployment environment and no merge request' do - let(:environment) { create(:environment, project: project, name: "foo-#{project.default_branch}") } - let(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) } - - it 'does not append CHANGED_PAGES variables' do - ci_variables = subject.select { |var| var[:key] =~ /MERGE_REQUEST_CHANGED_PAGES/ } - - expect(ci_variables).to be_empty - end - end - - context 'with no deployment environment and a present merge request' do - let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project, target_project: project) } - let(:build) { create(:ci_build, pipeline: merge_request.all_pipelines.take) } - - it 'does not append CHANGED_PAGES variables' do - ci_variables = subject.select { |var| var[:key] =~ /MERGE_REQUEST_CHANGED_PAGES/ } - - expect(ci_variables).to be_empty - end - end - - context 'with no deployment environment and no merge request' do - it 'does not append CHANGED_PAGES variables' do - ci_variables = subject.select { |var| var[:key] =~ /MERGE_REQUEST_CHANGED_PAGES/ } - - expect(ci_variables).to be_empty - end - end - end - - context 'with the :modified_path_ci_variables feature flag disabled' do - before do - stub_feature_flags(modified_path_ci_variables: false) - end - - it 'does not set CI_MERGE_REQUEST_CHANGED_PAGES_* variables' do - expect(subject.find { |var| var[:key] == 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS' }).to be_nil - expect(subject.find { |var| var[:key] == 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS' }).to be_nil - end - end - end - context 'when build has user' do let(:user_variables) do [ diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index a6954fb5d56..09a73a4cdcb 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -66,18 +66,6 @@ RSpec.describe EnvironmentStatus do end end - describe '#changed_paths' do - subject { environment_status.changed_urls } - - it { is_expected.to contain_exactly("#{environment.external_url}/ruby-style-guide.html", "#{environment.external_url}/html/page.html") } - end - - describe '#changed_urls' do - subject { environment_status.changed_paths } - - it { is_expected.to contain_exactly('ruby-style-guide.html', 'html/page.html') } - end - describe '.for_merge_request' do let(:admin) { create(:admin) } let!(:pipeline) { create(:ci_pipeline, sha: sha, merge_requests_as_head_pipeline: [merge_request]) } diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index 4fa95f8ebb2..2dc92417892 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -194,7 +194,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do [{ 'key' => 'cache_key', 'untracked' => false, 'paths' => ['vendor/*'], - 'policy' => 'pull-push' }] + 'policy' => 'pull-push', + 'when' => 'on_success' }] end let(:expected_features) { { 'trace_sections' => true } } diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index b04bae3e224..a683dc28f4f 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -306,12 +306,9 @@ RSpec.describe 'project routing' do end # raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw # project_snippets GET /:project_id/snippets(.:format) snippets#index - # POST /:project_id/snippets(.:format) snippets#create # new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new # edit_project_snippet GET /:project_id/snippets/:id/edit(.:format) snippets#edit # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show - # PUT /:project_id/snippets/:id(.:format) snippets#update - # DELETE /:project_id/snippets/:id(.:format) snippets#destroy describe SnippetsController, 'routing' do it 'to #raw' do expect(get('/gitlab/gitlabhq/-/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') @@ -321,10 +318,6 @@ RSpec.describe 'project routing' do expect(get('/gitlab/gitlabhq/-/snippets')).to route_to('projects/snippets#index', namespace_id: 'gitlab', project_id: 'gitlabhq') end - it 'to #create' do - expect(post('/gitlab/gitlabhq/-/snippets')).to route_to('projects/snippets#create', namespace_id: 'gitlab', project_id: 'gitlabhq') - end - it 'to #new' do expect(get('/gitlab/gitlabhq/-/snippets/new')).to route_to('projects/snippets#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end @@ -337,14 +330,6 @@ RSpec.describe 'project routing' do expect(get('/gitlab/gitlabhq/-/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end - it 'to #update' do - expect(put('/gitlab/gitlabhq/-/snippets/1')).to route_to('projects/snippets#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - - it 'to #destroy' do - expect(delete('/gitlab/gitlabhq/-/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - it 'to #show from unscope routing' do expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 6150a8b26cc..6b7a0d018f1 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -61,12 +61,9 @@ RSpec.describe "Mounted Apps", "routing" do end # snippets GET /snippets(.:format) snippets#index -# POST /snippets(.:format) snippets#create # new_snippet GET /snippets/new(.:format) snippets#new # edit_snippet GET /snippets/:id/edit(.:format) snippets#edit # snippet GET /snippets/:id(.:format) snippets#show -# PUT /snippets/:id(.:format) snippets#update -# DELETE /snippets/:id(.:format) snippets#destroy RSpec.describe SnippetsController, "routing" do it "to #raw" do expect(get("/-/snippets/1/raw")).to route_to('snippets#raw', id: '1') @@ -76,10 +73,6 @@ RSpec.describe SnippetsController, "routing" do expect(get("/-/snippets")).to route_to('snippets#index') end - it "to #create" do - expect(post("/-/snippets")).to route_to('snippets#create') - end - it "to #new" do expect(get("/-/snippets/new")).to route_to('snippets#new') end @@ -92,14 +85,6 @@ RSpec.describe SnippetsController, "routing" do expect(get("/-/snippets/1")).to route_to('snippets#show', id: '1') end - it "to #update" do - expect(put("/-/snippets/1")).to route_to('snippets#update', id: '1') - end - - it "to #destroy" do - expect(delete("/-/snippets/1")).to route_to('snippets#destroy', id: '1') - end - it 'to #show from unscoped routing' do expect(get("/snippets/1")).to route_to('snippets#show', id: '1') end diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb index 306a4fa43a9..e1734d5290f 100644 --- a/spec/serializers/discussion_entity_spec.rb +++ b/spec/serializers/discussion_entity_spec.rb @@ -79,13 +79,5 @@ RSpec.describe DiscussionEntity do :active ) end - - context 'diff_head_compare feature is disabled' do - it 'does not expose positions and line_codes attributes' do - stub_feature_flags(merge_ref_head_comments: false) - - expect(subject.keys).not_to include(:positions, :line_codes) - end - end end end diff --git a/spec/services/ci/create_pipeline_service/cache_spec.rb b/spec/services/ci/create_pipeline_service/cache_spec.rb index 614e46f1b1a..1438c2e4aa0 100644 --- a/spec/services/ci/create_pipeline_service/cache_spec.rb +++ b/spec/services/ci/create_pipeline_service/cache_spec.rb @@ -36,7 +36,8 @@ RSpec.describe Ci::CreatePipelineService do 'key' => 'a-key', 'paths' => ['logs/', 'binaries/'], 'policy' => 'pull-push', - 'untracked' => true + 'untracked' => true, + 'when' => 'on_success' } expect(pipeline).to be_persisted @@ -67,7 +68,8 @@ RSpec.describe Ci::CreatePipelineService do expected = { 'key' => /[a-f0-9]{40}/, 'paths' => ['logs/'], - 'policy' => 'pull-push' + 'policy' => 'pull-push', + 'when' => 'on_success' } expect(pipeline).to be_persisted @@ -82,7 +84,8 @@ RSpec.describe Ci::CreatePipelineService do expected = { 'key' => /default/, 'paths' => ['logs/'], - 'policy' => 'pull-push' + 'policy' => 'pull-push', + 'when' => 'on_success' } expect(pipeline).to be_persisted @@ -114,7 +117,8 @@ RSpec.describe Ci::CreatePipelineService do expected = { 'key' => /\$ENV_VAR-[a-f0-9]{40}/, 'paths' => ['logs/'], - 'policy' => 'pull-push' + 'policy' => 'pull-push', + 'when' => 'on_success' } expect(pipeline).to be_persisted @@ -129,7 +133,8 @@ RSpec.describe Ci::CreatePipelineService do expected = { 'key' => /\$ENV_VAR-default/, 'paths' => ['logs/'], - 'policy' => 'pull-push' + 'policy' => 'pull-push', + 'when' => 'on_success' } expect(pipeline).to be_persisted diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index fa70ad8c559..86e49fe601c 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -154,7 +154,7 @@ RSpec.describe MergeRequests::CreateFromIssueService do result = service.execute - expect(result[:merge_request].label_ids).to eq(label_ids) + expect(result[:merge_request].label_ids).to match_array(label_ids) end it "inherits milestones" do diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb index 543da46f883..bffd4c4d34d 100644 --- a/spec/services/merge_requests/mergeability_check_service_spec.rb +++ b/spec/services/merge_requests/mergeability_check_service_spec.rb @@ -41,16 +41,6 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar subject end - context 'when merge_ref_head_comments is disabled' do - it 'does not update diff discussion positions' do - stub_feature_flags(merge_ref_head_comments: false) - - expect(Discussions::CaptureDiffNotePositionsService).not_to receive(:new) - - subject - end - end - it 'updates the merge ref' do expect { subject }.to change(merge_request, :merge_ref_head).from(nil) end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 4da9f4115a1..1e5536a2d0b 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -163,14 +163,6 @@ RSpec.describe Notes::CreateService do expect(note.note_diff_file).to be_present expect(note.diff_note_positions).to be_present end - - it 'does not create diff positions merge_ref_head_comments is disabled' do - stub_feature_flags(merge_ref_head_comments: false) - - expect(Discussions::CaptureDiffNotePositionService).not_to receive(:new) - - described_class.new(project_with_repo, user, new_opts).execute - end end context 'when DiffNote is a reply' do diff --git a/spec/support/migrations_helpers/cluster_helpers.rb b/spec/support/migrations_helpers/cluster_helpers.rb index b54af15c29e..03104e22bcf 100644 --- a/spec/support/migrations_helpers/cluster_helpers.rb +++ b/spec/support/migrations_helpers/cluster_helpers.rb @@ -4,7 +4,7 @@ module MigrationHelpers module ClusterHelpers # Creates a list of cluster projects. def create_cluster_project_list(quantity) - group = namespaces_table.create(name: 'gitlab-org', path: 'gitlab-org') + group = namespaces_table.create!(name: 'gitlab-org', path: 'gitlab-org') quantity.times do |id| create_cluster_project(group, id) @@ -25,14 +25,14 @@ module MigrationHelpers namespace_id: group.id ) - cluster = clusters_table.create( + cluster = clusters_table.create!( name: 'test-cluster', cluster_type: 3, provider_type: :gcp, platform_type: :kubernetes ) - cluster_projects_table.create(project_id: project.id, cluster_id: cluster.id) + cluster_projects_table.create!(project_id: project.id, cluster_id: cluster.id) provider_gcp_table.create!( gcp_project_id: "test-gcp-project-#{id}", @@ -43,7 +43,7 @@ module MigrationHelpers zone: 'us-central1-a' ) - platform_kubernetes_table.create( + platform_kubernetes_table.create!( cluster_id: cluster.id, api_url: 'https://kubernetes.example.com', encrypted_token: 'a' * 40, @@ -58,7 +58,7 @@ module MigrationHelpers project = projects_table.find(cluster_project.project_id) namespace = "#{project.path}-#{project.id}" - cluster_kubernetes_namespaces_table.create( + cluster_kubernetes_namespaces_table.create!( cluster_project_id: cluster_project.id, cluster_id: cluster.id, project_id: cluster_project.project_id, diff --git a/spec/support/migrations_helpers/namespaces_helper.rb b/spec/support/migrations_helpers/namespaces_helper.rb index 4ca01c87568..c62ef6a4620 100644 --- a/spec/support/migrations_helpers/namespaces_helper.rb +++ b/spec/support/migrations_helpers/namespaces_helper.rb @@ -3,7 +3,7 @@ module MigrationHelpers module NamespacesHelpers def create_namespace(name, visibility, options = {}) - table(:namespaces).create({ + table(:namespaces).create!({ name: name, path: name, type: 'Group', diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb index b4d7722f03d..298e03162c4 100644 --- a/spec/support/shared_contexts/email_shared_context.rb +++ b/spec/support/shared_contexts/email_shared_context.rb @@ -21,7 +21,7 @@ end RSpec.shared_examples :reply_processing_shared_examples do context "when the user could not be found" do before do - user.destroy + user.destroy! end it "raises a UserNotFoundError" do diff --git a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb index 58ee48a98f1..2b6edb4c07d 100644 --- a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb @@ -18,8 +18,8 @@ RSpec.shared_context 'GroupProjectsFinder context' do let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) } before do - shared_project_1.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) - shared_project_2.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) - shared_project_3.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) + shared_project_1.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group) + shared_project_2.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group) + shared_project_3.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group) end end diff --git a/spec/support/shared_contexts/mailers/notify_shared_context.rb b/spec/support/shared_contexts/mailers/notify_shared_context.rb index de8c0d5d2b4..4b7d028410a 100644 --- a/spec/support/shared_contexts/mailers/notify_shared_context.rb +++ b/spec/support/shared_contexts/mailers/notify_shared_context.rb @@ -11,7 +11,7 @@ RSpec.shared_context 'gitlab email notification' do let(:new_user_address) { 'newguy@example.com' } before do - email = recipient.emails.create(email: "notifications@example.com") + email = recipient.emails.create!(email: "notifications@example.com") recipient.update_attribute(:notification_email, email.email) stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}") end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 018971e288c..fc9115a5ea1 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -56,7 +56,7 @@ RSpec.describe GitGarbageCollectWorker do it "flushes ref caches when the task if 'gc'" do expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original - expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:expire_branches_cache).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original @@ -77,7 +77,7 @@ RSpec.describe GitGarbageCollectWorker do end it 'returns silently' do - expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).not_to receive(:expire_branches_cache).and_call_original expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original @@ -102,7 +102,7 @@ RSpec.describe GitGarbageCollectWorker do it "flushes ref caches when the task if 'gc'" do expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{project.id}").and_return(false) - expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:expire_branches_cache).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original @@ -170,7 +170,7 @@ RSpec.describe GitGarbageCollectWorker do it 'returns silently' do expect(subject).not_to receive(:command) - expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).not_to receive(:expire_branches_cache).and_call_original expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original