diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-27 21:10:39 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-27 21:10:39 +0300 |
commit | 9beaa6816987274f2b870146ac649c970d69da24 (patch) | |
tree | 17af5519819903593a71b1eae47cbc0999f9a1c7 | |
parent | 524a21e75209d2501b23b648daf753e3a4bebe56 (diff) |
Add latest changes from gitlab-org/gitlab@master
87 files changed, 1406 insertions, 290 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 1db98e8532e..80cb590fde5 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -76,6 +76,9 @@ .if-merge-request-labels-run-all-jest: &if-merge-request-labels-run-all-jest if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-all-jest/' +.if-merge-request-labels-run-all-e2e: &if-merge-request-labels-run-all-e2e + if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-all-e2e/' + .if-merge-request-labels-run-single-db: &if-merge-request-labels-run-single-db if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-single-db/' @@ -297,6 +300,9 @@ - "vendor/assets/**/*" - "{app/assets,app/components,app/helpers,app/presenters,app/views,locale,public,spec/frontend,storybook,symbol}/**/*" +.initializers-patterns: &initializers-patterns + - "{,ee/,jh/}config/initializers/**/*" + .controllers-patterns: &controllers-patterns - "{,ee/,jh/}{app/controllers}/**/*" @@ -1141,6 +1147,8 @@ allow_failure: true - <<: *if-ruby2-branch allow_failure: true + - <<: *if-merge-request-labels-run-all-e2e + allow_failure: true - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e changes: *feature-flag-development-config-patterns when: manual @@ -1149,6 +1157,9 @@ changes: *feature-flag-development-config-patterns allow_failure: true - <<: *if-dot-com-gitlab-org-and-security-merge-request + changes: *initializers-patterns + allow_failure: true + - <<: *if-dot-com-gitlab-org-and-security-merge-request changes: *nodejs-patterns allow_failure: true - <<: *if-dot-com-gitlab-org-and-security-merge-request diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 2d734a19249..d0e55637190 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -b1841c6d79f62066335ad4d44efa21f3c5f77c03 +ddcce8f5e7878c997a9863f5c3ed532d7126256b @@ -39,7 +39,7 @@ gem 'default_value_for', '~> 3.4.0' # Supported DBs gem 'pg', '~> 1.4.5' -gem 'rugged', '~> 1.2' +gem 'rugged', '~> 1.5' gem 'grape-path-helpers', '~> 1.7.1' gem 'faraday', '~> 1.0' diff --git a/Gemfile.checksum b/Gemfile.checksum index e19a9ab3ab6..cdc16a72abf 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -519,7 +519,7 @@ {"name":"rubyntlm","version":"0.6.3","platform":"ruby","checksum":"5b321456dba3130351f7451f8669f1afa83a0d26fd63cdec285b7b88e667102d"}, {"name":"rubypants","version":"0.2.0","platform":"ruby","checksum":"f07e38eac793655a0323fe91946081052341b9e69807026fcf102346589eedee"}, {"name":"rubyzip","version":"2.3.2","platform":"ruby","checksum":"3f57e3935dc2255c414484fbf8d673b4909d8a6a57007ed754dde39342d2373f"}, -{"name":"rugged","version":"1.2.0","platform":"ruby","checksum":"111979962e75378209673ae262a8f43bd1971b5b5f96f4dfb77ca82b343604ed"}, +{"name":"rugged","version":"1.5.1","platform":"ruby","checksum":"a83493d050652d9e65eb6844a32f2c3da59e385e875214f7e502db547a7fce72"}, {"name":"safe_yaml","version":"1.0.4","platform":"ruby","checksum":"248193992ef1730a0c9ec579999ef2256a2b3a32a9bd9d708a1e12544a489ec2"}, {"name":"safety_net_attestation","version":"0.4.0","platform":"ruby","checksum":"96be2d74e7ed26453a51894913449bea0e072f44490021545ac2d1c38b0718ce"}, {"name":"sanitize","version":"6.0.0","platform":"ruby","checksum":"81795f985873f3bacee2eaaededeaafc3a29aafeaa9aff51e04b85a66bbf08ff"}, @@ -618,7 +618,7 @@ {"name":"tzinfo","version":"2.0.5","platform":"ruby","checksum":"c5352fd901544d396745d013f46a04ae2ed081ce806d942099825b7c2b09a167"}, {"name":"u2f","version":"0.2.1","platform":"ruby","checksum":"7907b163c00682ce94d82178154af2ec3930e50f342c3502d64929c6370c5553"}, {"name":"uber","version":"0.1.0","platform":"ruby","checksum":"5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc"}, -{"name":"undercover","version":"0.4.4","platform":"ruby","checksum":"61c4cbe03a9de0764b07cceef82a63f9d8dbf4d8680ec017cee307927561f6a5"}, +{"name":"undercover","version":"0.4.5","platform":"ruby","checksum":"27bb5d708e253e2c1a3f3edd3668a30728f0e59a1e18004b623d5e7b1e86f3b9"}, {"name":"unf","version":"0.1.4","platform":"java","checksum":"49a5972ec0b3d091d3b0b2e00113f2f342b9b212f0db855eb30a629637f6d302"}, {"name":"unf","version":"0.1.4","platform":"ruby","checksum":"4999517a531f2a955750f8831941891f6158498ec9b6cb1c81ce89388e63022e"}, {"name":"unf_ext","version":"0.0.8.2","platform":"ruby","checksum":"90b9623ee359cc4878461c5d2eab7d3d3ce5801a680a9e7ac83b8040c5b742fa"}, diff --git a/Gemfile.lock b/Gemfile.lock index e5e54e4b654..8948e8606d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1304,7 +1304,7 @@ GEM rubyntlm (0.6.3) rubypants (0.2.0) rubyzip (2.3.2) - rugged (1.2.0) + rugged (1.5.1) safe_yaml (1.0.4) safety_net_attestation (0.4.0) jwt (~> 2.0) @@ -1501,10 +1501,10 @@ GEM concurrent-ruby (~> 1.0) u2f (0.2.1) uber (0.1.0) - undercover (0.4.4) + undercover (0.4.5) imagen (>= 0.1.8) rainbow (>= 2.1, < 4.0) - rugged (>= 0.27, < 1.3) + rugged (>= 0.27, < 1.6) unf (0.1.4) unf_ext unf_ext (0.0.8.2) @@ -1820,7 +1820,7 @@ DEPENDENCIES ruby-saml (~> 1.13.0) ruby_parser (~> 3.19) rubyzip (~> 2.3.2) - rugged (~> 1.2) + rugged (~> 1.5) sanitize (~> 6.0) sassc-rails (~> 2.1.0) sd_notify (~> 0.1.0) diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js index 5b5abbdf50b..fe2e2bde940 100644 --- a/app/assets/javascripts/api/projects_api.js +++ b/app/assets/javascripts/api/projects_api.js @@ -19,6 +19,10 @@ export function getProjects(query, options, callback = () => {}) { defaults.membership = true; } + if (gon.features.fullPathProjectSearch && query?.includes('/')) { + defaults.search_namespaces = true; + } + return axios .get(url, { params: Object.assign(defaults, options), diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js index 130a8785250..745a03f411a 100644 --- a/app/assets/javascripts/graphql_shared/issuable_client.js +++ b/app/assets/javascripts/graphql_shared/issuable_client.js @@ -74,6 +74,7 @@ export const config = { }, }; } + return incomingWidget || existingWidget; }); }, diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js index cda3379309c..65e113e5084 100644 --- a/app/assets/javascripts/header_search/constants.js +++ b/app/assets/javascripts/header_search/constants.js @@ -77,3 +77,5 @@ export const DROPDOWN_ORDER = [ ]; export const FETCH_TYPES = ['generic', 'search']; + +export const SEARCH_INPUT_FIELD_MAX_WIDTH = '640px'; diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js index f6f5c6a14fa..f6963263725 100644 --- a/app/assets/javascripts/header_search/index.js +++ b/app/assets/javascripts/header_search/index.js @@ -1,36 +1,44 @@ import Vue from 'vue'; +import * as Sentry from '@sentry/browser'; import Translate from '~/vue_shared/translate'; import HeaderSearchApp from './components/app.vue'; import createStore from './store'; +import { SEARCH_INPUT_FIELD_MAX_WIDTH } from './constants'; Vue.use(Translate); export const initHeaderSearchApp = (search = '') => { const el = document.getElementById('js-header-search'); - let navBarEl = null; + const headerEl = document.querySelector('.header-content'); - if (!el) { + if (!el && !headerEl) { return false; } + const searchContainer = headerEl.querySelector('.global-search-container'); + const newHeader = headerEl.querySelector('.header-search-new'); + const { searchPath, issuesPath, mrPath, autocompletePath } = el.dataset; let { searchContext } = el.dataset; - searchContext = JSON.parse(searchContext); + + try { + searchContext = JSON.parse(searchContext); + newHeader.style.maxWidth = SEARCH_INPUT_FIELD_MAX_WIDTH; + } catch (error) { + Sentry.captureException(error); + } return new Vue({ el, store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }), - mounted() { - navBarEl = document.querySelector('.header-content'); - }, render(createElement) { return createElement(HeaderSearchApp, { on: { expandSearchBar: () => { - navBarEl?.classList.add('header-search-is-active'); + searchContainer.style.flexGrow = '1'; }, collapseSearchBar: () => { - navBarEl?.classList.remove('header-search-is-active'); + searchContainer.style.flexGrow = '0'; }, }, }); diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js index 01bc5dfc66b..bb22a4ef252 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/constants.js +++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js @@ -38,7 +38,7 @@ export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_ anchor: 'use-the-integration', }); export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('integration/jira/connect-app', { - anchor: 'connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances', + anchor: 'connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances', }); export const GITLAB_COM_BASE_PATH = 'https://gitlab.com'; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 3894ec36a0b..05ed08931bb 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -522,15 +522,23 @@ function handleContinueList(e, textArea) { if (!(e.key === 'Enter')) return; if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; if (textArea.selectionStart !== textArea.selectionEnd) return; + // prevent unintended line breaks inserted using Japanese IME on MacOS if (compositioningNoteText) return; - const firstSelectedLine = linesFromSelection(textArea).lines[0]; + const selectedLines = linesFromSelection(textArea); + const firstSelectedLine = selectedLines.lines[0]; const listLineMatch = firstSelectedLine.match(LIST_LINE_HEAD_PATTERN); if (listLineMatch) { const { leader, indent, content, isOl } = listLineMatch.groups; const emptyListItem = !content; + const prefixLength = leader.length + indent.length; + + if (selectedLines.selectionStart - selectedLines.startPos < prefixLength) { + // cursor in the indent/leader area, allow the natural line feed to be added + return; + } if (emptyListItem) { // erase empty list item - select the text and allow the diff --git a/app/assets/javascripts/pages/admin/hooks/index.js b/app/assets/javascripts/pages/admin/hooks/index.js new file mode 100644 index 00000000000..82e601426f1 --- /dev/null +++ b/app/assets/javascripts/pages/admin/hooks/index.js @@ -0,0 +1,3 @@ +import { initHookTestDropdowns } from '~/webhooks'; + +initHookTestDropdowns(); diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js index 9e559354205..f25547f9982 100644 --- a/app/assets/javascripts/pages/projects/hooks/index.js +++ b/app/assets/javascripts/pages/projects/hooks/index.js @@ -1,7 +1,8 @@ import initSearchSettings from '~/search_settings'; -import initWebhookForm from '~/webhooks'; +import initWebhookForm, { initHookTestDropdowns } from '~/webhooks'; import { initPushEventsEditForm } from '~/webhooks/webhook'; initSearchSettings(); initWebhookForm(); initPushEventsEditForm(); +initHookTestDropdowns(); diff --git a/app/assets/javascripts/tracking/get_standard_context.js b/app/assets/javascripts/tracking/get_standard_context.js index 6014f1ba3ee..df527e24d93 100644 --- a/app/assets/javascripts/tracking/get_standard_context.js +++ b/app/assets/javascripts/tracking/get_standard_context.js @@ -10,7 +10,7 @@ export default function getStandardContext({ extra = {} } = {}) { ...data, source: SNOWPLOW_JS_SOURCE, google_analytics_id: getCookie(GOOGLE_ANALYTICS_ID_COOKIE_NAME) ?? '', - extra: extra || data.extra, + extra: { ...data.extra, ...extra }, }, }; } diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 8e459cc21ac..1be6906b77c 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -4,7 +4,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html'; import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { glEmojiTag } from '~/emoji'; import { __, sprintf } from '~/locale'; -import CiIconBadge from './ci_badge_link.vue'; +import CiBadgeLink from './ci_badge_link.vue'; import TimeagoTooltip from './time_ago_tooltip.vue'; /** @@ -16,7 +16,7 @@ import TimeagoTooltip from './time_ago_tooltip.vue'; */ export default { components: { - CiIconBadge, + CiBadgeLink, TimeagoTooltip, GlButton, GlAvatarLink, @@ -120,7 +120,7 @@ export default { data-testid="ci-header-content" > <section class="header-main-content gl-mr-3"> - <ci-icon-badge :status="status" /> + <ci-badge-link :status="status" /> <strong data-testid="ci-header-item-text">{{ item }}</strong> diff --git a/app/assets/javascripts/webhooks/components/test_dropdown.vue b/app/assets/javascripts/webhooks/components/test_dropdown.vue new file mode 100644 index 00000000000..78e5dff6f59 --- /dev/null +++ b/app/assets/javascripts/webhooks/components/test_dropdown.vue @@ -0,0 +1,69 @@ +<script> +import { GlDisclosureDropdown } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + name: 'HookTestDropdown', + components: { + GlDisclosureDropdown, + }, + props: { + items: { + type: Array, + required: true, + }, + size: { + type: String, + required: false, + default: undefined, + }, + }, + computed: { + itemsWithAction() { + return this.items.map((item) => ({ + text: item.text, + action: () => this.testHook(item.href), + })); + }, + }, + methods: { + testHook(href) { + // HACK: Trigger @rails/ujs's data-method handling. + // + // The more obvious approaches of (1) declaratively rendering the + // links using GlDisclosureDropdown's list-item slot and (2) using + // item.extraAttrs to set the data-method attributes on the links + // do not work for reasons laid out in + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2134. + // + // Sending the POST with axios also doesn't work, since the + // endpoints return 302 redirects. Since axios uses XMLHTTPRequest, + // it transparently follows redirects, meaning the Location header + // of the first response cannot be inspected/acted upon by JS. We + // could manually trigger a reload afterwards, but that would mean + // a duplicate fetch of the current page: one by the XHR, and one + // by the explicit reload. It would also mean losing the flash + // alert set by the backend, making the feature useless for the + // user. + // + // The ideal fix here would be to refactor the test endpoint to + // return a JSON response, removing the need for a redirect/page + // reload to show the result. + const a = document.createElement('a'); + a.setAttribute('hidden', ''); + a.href = href; + a.dataset.method = 'post'; + document.body.appendChild(a); + a.click(); + a.remove(); + }, + }, + i18n: { + test: __('Test'), + }, +}; +</script> + +<template> + <gl-disclosure-dropdown :toggle-text="$options.i18n.test" :items="itemsWithAction" :size="size" /> +</template> diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js index 7d04978280b..6eb7cbea72c 100644 --- a/app/assets/javascripts/webhooks/index.js +++ b/app/assets/javascripts/webhooks/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import FormUrlApp from './components/form_url_app.vue'; +import TestDropdown from './components/test_dropdown.vue'; export default () => { const el = document.querySelector('.js-vue-webhook-form'); @@ -23,3 +24,22 @@ export default () => { }, }); }; + +const initHookTestDropdown = (el) => { + const { items, size } = el.dataset; + + return new Vue({ + el, + render(h) { + return h(TestDropdown, { + props: { + items: JSON.parse(items), + size, + }, + }); + }, + }); +}; + +export const initHookTestDropdowns = (selector = '.js-webhook-test-dropdown') => + document.querySelectorAll(selector).forEach(initHookTestDropdown); diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue index 171ebd13c29..1b8e97bf717 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue @@ -156,6 +156,7 @@ export default { modal-id="work-item-detail-modal" header-class="gl-p-0 gl-pb-2!" scrollable + data-testid="work-item-detail-modal" @hide="closeModal" > <gl-alert v-if="error" variant="danger" @dismiss="error = false"> diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql index b7813ca4dc6..b5d27231bef 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql @@ -3,6 +3,15 @@ #import "~/work_items/graphql/milestone.fragment.graphql" fragment WorkItemMetadataWidgets on WorkItemWidget { + ... on WorkItemWidgetDescription { + type + } + ... on WorkItemWidgetStartAndDueDate { + type + } + ... on WorkItemWidgetNotes { + type + } ... on WorkItemWidgetMilestone { type milestone { @@ -11,6 +20,8 @@ fragment WorkItemMetadataWidgets on WorkItemWidget { } ... on WorkItemWidgetAssignees { type + allowsMultipleAssignees + canInviteMembers assignees { nodes { ...User diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 4b1efcc1e9a..f13213621e1 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -1,5 +1,4 @@ $search-input-field-min-width: 320px; -$search-input-field-max-width: 640px; $search-input-field-x-min-width: 200px; .navbar-gitlab { @@ -80,16 +79,6 @@ $search-input-field-x-min-width: 200px; .navbar-collapse > ul.nav > li:not(.d-none) { margin: 0 2px; } - - .header-search-new { - max-width: $search-input-field-max-width; - } - - &.header-search-is-active { - .global-search-container { - flex-grow: 1; - } - } } .header-search { diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index c9425b1103c..422a2cc350d 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -837,9 +837,6 @@ kbd { .navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { margin: 0 2px; } -.navbar-gitlab .header-content .header-search-new { - max-width: 640px; -} .navbar-gitlab .header-search { min-width: 320px; } diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 40a6e9eb9fe..f7f6e36eb31 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -837,9 +837,6 @@ kbd { .navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { margin: 0 2px; } -.navbar-gitlab .header-content .header-search-new { - max-width: 640px; -} .navbar-gitlab .header-search { min-width: 320px; } diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb index bf40c12aec5..320aa423ce3 100644 --- a/app/graphql/mutations/merge_requests/set_milestone.rb +++ b/app/graphql/mutations/merge_requests/set_milestone.rb @@ -17,7 +17,7 @@ module Mutations merge_request = authorized_find!(project_path: project_path, iid: iid) project = merge_request.project - ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { milestone: milestone }) + ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { milestone_id: milestone&.id }) .execute(merge_request) { diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb index 63544e28a0e..ac1e4456bc7 100644 --- a/app/helpers/hooks_helper.rb +++ b/app/helpers/hooks_helper.rb @@ -8,12 +8,12 @@ module HooksHelper } end - def link_to_test_hook(hook, trigger) - path = test_hook_path(hook, trigger) - trigger_human_name = integration_webhook_event_human_name(trigger) - - link_to path, rel: 'nofollow', method: :post do - content_tag(:span, trigger_human_name) + def webhook_test_items(hook, triggers) + triggers.map do |trigger| + { + href: test_hook_path(hook, trigger), + text: integration_webhook_event_human_name(trigger) + } end end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 9eb3308b901..0a968be747a 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -183,4 +183,11 @@ class Discussion resolved_at ].join(':') end + + # Consolidate discussions GID. There is no need to have different GID for different class names as the discussion_id + # hash is already unique per discussion. This also fixes the issue where same discussion may return different GIDs + # depending on number of notes it has. + def to_global_id(options = {}) + GlobalID.new(::Gitlab::GlobalId.build(model_name: Discussion.to_s, id: id)) + end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e24ae8f59f0..911d04d6b7a 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -34,8 +34,9 @@ class IssuableBaseService < ::BaseProjectService end def filter_params(issuable) + params.delete(:milestone) + unless can_set_issuable_metadata?(issuable) - params.delete(:milestone) params.delete(:milestone_id) params.delete(:labels) params.delete(:add_label_ids) diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb index 711978dc3f7..235dc6678df 100644 --- a/app/services/merge_requests/push_options_handler_service.rb +++ b/app/services/merge_requests/push_options_handler_service.rb @@ -145,7 +145,7 @@ module MergeRequests if push_options[:milestone] milestone = Milestone.for_projects_and_groups(@project, @project.ancestors_upto)&.find_by_name(push_options[:milestone]) - params[:milestone] = milestone if milestone + params[:milestone_id] = milestone.id if milestone end if params.key?(:description) diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb index 2563f2f5390..c348e2a58a3 100644 --- a/app/services/milestones/destroy_service.rb +++ b/app/services/milestones/destroy_service.rb @@ -4,7 +4,7 @@ module Milestones class DestroyService < Milestones::BaseService def execute(milestone) Milestone.transaction do - update_params = { milestone: nil, skip_milestone_email: true } + update_params = { milestone_id: nil, skip_milestone_email: true } milestone.issues.each do |issue| Issues::UpdateService.new(project: parent, current_user: current_user, params: update_params).execute(issue) diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml index 0b5c4730b64..5db7f22e36b 100644 --- a/app/views/layouts/_snowplow.html.haml +++ b/app/views/layouts/_snowplow.html.haml @@ -15,6 +15,7 @@ gl.snowplowStandardContext = #{Gitlab::Tracking::StandardContext.new( namespace: namespace, project: @project, - user: current_user + user: current_user, + new_nav: show_super_sidebar? ).to_context.to_json.to_json} gl.snowplowPseudonymizedPageUrl = #{masked_page_url(group: namespace, project: @project).to_json}; diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml index c19b518acd6..155a7b1827f 100644 --- a/app/views/shared/web_hooks/_hook.html.haml +++ b/app/views/shared/web_hooks/_hook.html.haml @@ -19,7 +19,9 @@ = gl_badge_tag(integration_webhook_event_human_name(trigger), size: :sm) = gl_badge_tag(sslBadgeText, size: :sm) - .col-md-4.col-lg-5.text-right-md.gl-mt-2 - %span>= render 'shared/web_hooks/test_button', hook: hook, button_class: 'btn-sm btn-default gl-mr-3' - %span>= link_to _('Edit'), edit_hook_path(hook), class: 'btn gl-button btn-default btn-sm gl-mr-3' - = link_to _('Delete'), destroy_hook_path(hook), aria: { label: s_('Webhooks|Delete webhook') }, data: { confirm_btn_variant: "danger", confirm: s_('Webhooks|Are you sure you want to delete this webhook?') }, method: :delete, class: 'btn gl-button btn-secondary btn-danger-secondary btn-sm' + .col-md-4.col-lg-5.gl-mt-2.gl-display-flex.gl-md-justify-content-end.gl-align-items-baseline.gl-gap-3 + = render 'shared/web_hooks/test_button', hook: hook, size: 'small' + = render Pajamas::ButtonComponent.new(href: edit_hook_path(hook), size: :small) do + = _('Edit') + = render Pajamas::ButtonComponent.new(href: destroy_hook_path(hook), category: :secondary, variant: :danger, size: :small, method: :delete, button_options: { 'aria-label' => s_('Webhooks|Delete webhook'), data: { confirm_btn_variant: "danger", confirm: s_('Webhooks|Are you sure you want to delete this webhook?') } }) do + = _('Delete') diff --git a/app/views/shared/web_hooks/_test_button.html.haml b/app/views/shared/web_hooks/_test_button.html.haml index 7a78a32fe87..dbd26e5bd07 100644 --- a/app/views/shared/web_hooks/_test_button.html.haml +++ b/app/views/shared/web_hooks/_test_button.html.haml @@ -1,13 +1,5 @@ -- button_class = local_assigns.fetch(:button_class, '') - hook = local_assigns.fetch(:hook) -- triggers = hook.class.triggers +- size = local_assigns.fetch(:size, 'medium') +- triggers = hook.class.triggers.each_value -.hook-test-button.dropdown.gl-dropdown.inline> - %button.btn.gl-button{ 'data-toggle' => 'dropdown', class: button_class } - = _('Test') - = sprite_icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } - .gl-dropdown-inner - - triggers.each_value do |event| - %li.gl-dropdown-item - = link_to_test_hook(hook, event) +.js-webhook-test-dropdown{ data: { items: webhook_test_items(hook, triggers).to_json, size: size } } diff --git a/config/feature_flags/development/full_path_project_search.yml b/config/feature_flags/development/full_path_project_search.yml new file mode 100644 index 00000000000..83c52a20995 --- /dev/null +++ b/config/feature_flags/development/full_path_project_search.yml @@ -0,0 +1,8 @@ +--- +name: full_path_project_search +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108906 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388473 +milestone: '15.9' +type: development +group: group::threat insights +default_enabled: false diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 14762812b16..ea768b20990 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -774,6 +774,9 @@ Gitlab.ee do Settings.cron_jobs['elastic_migration_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['elastic_migration_worker']['cron'] ||= '*/30 * * * *' Settings.cron_jobs['elastic_migration_worker']['job_class'] ||= 'Elastic::MigrationWorker' + Settings.cron_jobs['search_index_curation_worker'] ||= Settingslogic.new({}) + Settings.cron_jobs['search_index_curation_worker']['cron'] ||= '*/30 * * * *' + Settings.cron_jobs['search_index_curation_worker']['job_class'] ||= 'Search::IndexCurationWorker' Settings.cron_jobs['sync_seat_link_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['sync_seat_link_worker']['cron'] ||= "#{rand(60)} #{rand(3..4)} * * * UTC" Settings.cron_jobs['sync_seat_link_worker']['job_class'] = 'SyncSeatLinkWorker' diff --git a/data/whats_new/202212200001_15_07.yml b/data/whats_new/202212200001_15_07.yml index 937a4f76375..ebfcc0f16ad 100644 --- a/data/whats_new/202212200001_15_07.yml +++ b/data/whats_new/202212200001_15_07.yml @@ -110,7 +110,7 @@ self-managed: true gitlab-com: false available_in: [Free, Premium, Ultimate] - documentation_link: https://docs.gitlab.com/ee/integration/jira/connect-app.html#connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances + documentation_link: https://docs.gitlab.com/ee/integration/jira/connect-app.html#connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances image_url: https://about.gitlab.com/images/15_7/jira_cloud_app_proxy_for_selfmanaged_gitlab_users.png published_at: 2022-12-22 release: 15.7 diff --git a/doc/api/commits.md b/doc/api/commits.md index 609965f341d..5a3481ee086 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -753,7 +753,7 @@ Example response: ## List merge requests associated with a commit -Get a list of merge requests related to the specified commit. +Returns information about the merge request that originally introduced a specific commit. ```plaintext GET /projects/:id/repository/commits/:sha/merge_requests diff --git a/doc/api/settings.md b/doc/api/settings.md index 6ca94f840a2..6d92ceec285 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -524,8 +524,8 @@ listed in the descriptions of the relevant settings. | `whats_new_variant` | string | no | What's new variant, possible values: `all_tiers`, `current_tier`, and `disabled`. | | `web_ide_clientside_preview_enabled` | boolean | no | Live Preview (allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview). | | `wiki_page_max_content_bytes` | integer | no | Maximum wiki page content size in **bytes**. Default: 52428800 Bytes (50 MB). The minimum value is 1024 bytes. | -| `jira_connect_application_key` | String | no | Application ID of the OAuth application that should be used to authenticate with the GitLab.com for Jira Cloud app | -| `jira_connect_proxy_url` | String | no | URL of the GitLab instance that should be used as a proxy for the GitLab.com for Jira Cloud app | +| `jira_connect_application_key` | String | no | Application ID of the OAuth application that should be used to authenticate with the GitLab for Jira Cloud app | +| `jira_connect_proxy_url` | String | no | URL of the GitLab instance that should be used as a proxy for the GitLab for Jira Cloud app | ## Housekeeping fields diff --git a/doc/development/database/new_database_migration_version.md b/doc/development/database/new_database_migration_version.md new file mode 100644 index 00000000000..b97ecd83f37 --- /dev/null +++ b/doc/development/database/new_database_migration_version.md @@ -0,0 +1,64 @@ +--- +stage: Data Stores +group: Database +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Introducing a new database migration version + +At GitLab we've added many helpers for the database migrations to help developers manipulate +the schema and data of tables on a large scale like on GitLab.com. To avoid the repetitive task +of including the helpers into each database migration, we use a subclass of the Rails `ActiveRecord::Migration` +class that we use for all of our database migrations. This subclass is `Gitlab::Database::Migration`, and it already +includes all the helpers that developers can use. You can see many use cases of helpers built +in-house in [Avoiding downtime in migrations](avoiding_downtime_in_migrations.md). + +Sometimes, we need to add or modify existing an helper's functionality without having a reverse effect on all the +previous database migrations. That's why we introduced versioning to `Gitlab::Database::Migration`. Now, +each database migration can inherit the latest version of this class at the time of the writing the database migration. +After we add a new feature, those old database migrations are no longer affected. We usually +refer to the version using `Gitlab::Database::Migration[2.1]`, where `2.1` is the current version. + +Because we are chasing a moving target, adding a new migration and deprecating older versions +can be challenging. Database migrations are introduced every day, and this can break the pipeline. +In this document, we explain a two-step method to add a new database migration version. + +1. [Introduce a new version, and keep the older version allowed](#introduce-a-new-version-and-keep-the-older-version-allowed) +1. [Prevent the usage of the older database migration version](#prevent-the-usage-of-the-older-database-migration-version) + +## Introduce a new version, and keep the older version allowed + +1. The new version can be added to the + [`migration.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/migration.rb) + class, along with any new helpers that should be included in the new version. + Make sure that `current_version` refers to this new version. For example: + + ```ruby + class V2_2 < V2_1 # rubocop:disable Naming/ClassAndModuleCamelCase + include Gitlab::Database::MigrationHelpers::ANY_NEW_HELPER + end + + def self.current_version + 2.2 + end + ``` + +1. Update all the examples in the documentation to refer to the new database + migration version `Gitlab::Database::Migration[2.2]`. +1. Make sure that [`migration_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/db/migration_spec.rb) + doesn't fail for the new database migrations by adding an open date rate for + the **new database version**. + +## Prevent the usage of the older database migration version + +After some time passes, and ensuring all developers are using the +new database migration version in their merge requests, prevent the older +version from being used: + +1. Close the date range in [`migration_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/db/migration_spec.rb) + for the older database version. +1. Modify the + [`RuboCop::Cop::Migration::VersionedMigrationClass`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/cop/migration/versioned_migration_class.rb) + and [its owned tests](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/rubocop/cop/migration/versioned_migration_class_spec.rb). +1. Communicate this change on our Slack `#backend` and `#database` channels and + [Engineering Week-in-Review document](https://about.gitlab.com/handbook/engineering/#communication). diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 72263699a4c..c099eadc69c 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -1052,6 +1052,14 @@ The search results are displayed on a search page. Searching is different from [filtering](#filter). +## seats + +When referring to the subscription billing model: + +- For GitLab SaaS, use **seats**. Customers purchase seats. Users occupy seats when they are invited +to a group, with some [exceptions](../../../subscriptions/gitlab_com/index.md#how-seat-usage-is-determined). +- For GitLab self-managed, use **users**. Customers purchase subscriptions for a specified number of **users**. + ## section Use **section** to describe an area on a page. For example, if a page has lines that separate the UI diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md index 5b460f8723a..eca4d9775c5 100644 --- a/doc/development/integrations/jira_connect.md +++ b/doc/development/integrations/jira_connect.md @@ -54,7 +54,7 @@ To install the app in Jira: 1. Select **Upload**. - If the install was successful, you should see the **GitLab.com for Jira Cloud** app under **Manage apps**. + If the install was successful, you should see the **GitLab for Jira Cloud** app under **Manage apps**. You can also select **Getting Started** to open the configuration page rendered from your GitLab instance. _Note that any changes to the app descriptor requires you to uninstall then reinstall the app._ diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md index 07c5db908b0..d3ba8ea1561 100644 --- a/doc/development/pipelines/index.md +++ b/doc/development/pipelines/index.md @@ -188,7 +188,7 @@ Note that the merge request also needs to have the `master:broken` or `master:fo To make your Revert MRs faster, use the [revert MR template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/merge_request_templates/Revert%20To%20Resolve%20Incident.md) **before** you create your merge request. It will apply the `pipeline:expedite` label and others that will expedite the pipelines that run on the merge request. -### The `~pipeline:expedite` label +### The `pipeline:expedite` label When this label is assigned, the following steps of the CI/CD pipeline are skipped: @@ -207,11 +207,31 @@ If you want to force all the RSpec jobs to run regardless of your changes, you c WARNING: Forcing all jobs on docs only related MRs would not have the prerequisite jobs and would lead to errors +### End-to-end jobs + +The [`e2e:package-and-test`](../testing_guide/end_to_end/index.md#using-the-package-and-test-job) child pipeline +runs end-to-end jobs automatically depending on changes, and is manual in other cases. +See `.qa:rules:package-and-test` in +[`rules.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/rules.gitlab-ci.yml) for +the specific list of rules. + +If you want to force `e2e:package-and-test` to run regardless of your changes, you can add the +`pipeline:run-all-e2e` label to the merge request. + +Consult the [End-to-end Testing](../testing_guide/end_to_end/index.md) dedicated page for more information. + ### Review app jobs -Consult the [Review Apps](../testing_guide/review_apps.md) dedicated page for more information. +The [`start-review-app-pipeline`](../testing_guide/review_apps.md) child pipeline deploys a Review App and runs +end-to-end tests against it automatically depending on changes, and is manual in other cases. +See `.review:rules:start-review-app-pipeline` in +[`rules.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/rules.gitlab-ci.yml) for +the specific list of rules. + +If you want to force a Review App to be deployed regardless of your changes, you can add the +`pipeline:run-review-app` label to the merge request. -If you want to force a Review App to be deployed regardless of your changes, you can add the `pipeline:run-review-app` label to the merge request. +Consult the [Review Apps](../testing_guide/review_apps.md) dedicated page for more information. ### As-if-FOSS jobs @@ -380,13 +400,13 @@ fail. The `rspec:undercoverage` job has [known bugs](https://gitlab.com/groups/gitlab-org/-/epics/8254) that can cause false positive failures. You can test coverage locally to determine if it's -safe to apply `~"pipeline:skip-undercoverage"`. For example, using `<spec>` as the name of the +safe to apply `pipeline:skip-undercoverage`. For example, using `<spec>` as the name of the test causing the failure: 1. Run `SIMPLECOV=1 bundle exec rspec <spec>`. 1. Run `scripts/undercoverage`. -If these commands return `undercover: ✅ No coverage is missing in latest changes` then you can apply `~"pipeline:skip-undercoverage"` to bypass pipeline failures. +If these commands return `undercover: ✅ No coverage is missing in latest changes` then you can apply `pipeline:skip-undercoverage` to bypass pipeline failures. ## Test suite parallelization @@ -416,7 +436,7 @@ After that, the next pipeline uses the up-to-date `knapsack/report-master.json` ### Automatic skipping of flaky tests Tests that are [known to be flaky](../testing_guide/flaky_tests.md#automatic-retries-and-flaky-tests-detection) are -skipped unless the `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is set to `false` or if the `~"pipeline:run-flaky-tests"` +skipped unless the `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is set to `false` or if the `pipeline:run-flaky-tests` label is set on the MR. See the [experiment issue](https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1069). diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index 04fd36079ec..bca74d16f91 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -4,36 +4,36 @@ group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- -# GitLab.com for Jira Cloud app **(FREE)** +# GitLab for Jira Cloud app **(FREE)** You can integrate GitLab and Jira Cloud using the -[GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) +[GitLab for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) app in the Atlassian Marketplace. Only Jira users with administrator access can install or configure -the GitLab.com for Jira Cloud app. +the GitLab for Jira Cloud app. -## Install the GitLab.com for Jira Cloud app **(FREE SAAS)** +## Install the GitLab for Jira Cloud app **(FREE SAAS)** -If you use GitLab.com and Jira Cloud, you can install the GitLab.com for Jira Cloud app. +If you use GitLab.com and Jira Cloud, you can install the GitLab for Jira Cloud app. If you do not use both of these environments, use the [Jira DVCS Connector](dvcs/index.md) or -[install the GitLab.com for Jira Cloud app manually](#install-the-gitlabcom-for-jira-cloud-app-manually). -We recommend the GitLab.com for Jira Cloud app, because data is +[install the GitLab for Jira Cloud app manually](#install-the-gitlab-for-jira-cloud-app-manually). +We recommend the GitLab for Jira Cloud app, because data is synchronized in real time. The DVCS connector updates data only once per hour. -To configure the GitLab.com for Jira Cloud app, you must have +To configure the GitLab for Jira Cloud app, you must have at least the Maintainer role in the GitLab.com namespace. This integration method supports [Smart Commits](dvcs/index.md#smart-commits). <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -For a walkthrough of the integration with GitLab.com for Jira Cloud app, watch +For a walkthrough of the integration with GitLab for Jira Cloud app, watch [Configure GitLab.com Jira Could Integration using Marketplace App](https://youtu.be/SwR-g1s1zTo) on YouTube. -To install the GitLab.com for Jira Cloud app: +To install the GitLab for Jira Cloud app: 1. In Jira, go to **Jira Settings > Apps > Find new apps**, then search for GitLab. -1. Select **GitLab.com for Jira Cloud**, then select **Get it now**, or go to the +1. Select **GitLab for Jira Cloud**, then select **Get it now**, or go to the [App in the marketplace directly](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud). ![Install GitLab.com app on Jira Cloud](img/jira_dev_panel_setup_com_1.png) @@ -44,14 +44,14 @@ To install the GitLab.com for Jira Cloud app: 1. To add namespaces, ensure you're signed in to GitLab.com as a user with at least the Maintainer role. - ![Sign in to GitLab.com in GitLab.com for Jira Cloud app](img/jira_dev_panel_setup_com_3_v13_9.png) + ![Sign in to GitLab.com in GitLab for Jira Cloud app](img/jira_dev_panel_setup_com_3_v13_9.png) 1. To open the list of available namespaces, select **Add namespace**. 1. Identify the namespace you want to link, and select **Link**. - You must have at least the Maintainer role for the namespace. - Only Jira site administrators can add or remove namespaces for an installation. - ![Link namespace in GitLab.com for Jira Cloud app](img/jira_dev_panel_setup_com_4_v13_9.png) + ![Link namespace in GitLab for Jira Cloud app](img/jira_dev_panel_setup_com_4_v13_9.png) NOTE: The GitLab.com user only needs access when adding a new namespace. For syncing with @@ -65,7 +65,7 @@ After a namespace is added: Support for syncing past branch and commit data is tracked [in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/263240). -## Update the GitLab.com for Jira Cloud app +## Update the GitLab for Jira Cloud app Most updates to the app are fully automated and don't require any user interaction. See the [Atlassian Marketplace documentation](https://developer.atlassian.com/platform/marketplace/upgrading-and-versioning-cloud-apps/) @@ -75,13 +75,13 @@ If the app requires additional permissions, [the update must first be manually a ## Set up OAuth authentication -The GitLab.com for Jira Cloud app is [switching to OAuth authentication](https://gitlab.com/gitlab-org/gitlab/-/issues/387299). +The GitLab for Jira Cloud app is [switching to OAuth authentication](https://gitlab.com/gitlab-org/gitlab/-/issues/387299). To enable OAuth authentication, you must create an OAuth application on the GitLab instance. Enabling OAuth authentication is: -- Required to [connect the GitLab.com for Jira Cloud app for self-managed instances](#connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances). -- Recommended to [install the GitLab.com for Jira Cloud app manually](#install-the-gitlabcom-for-jira-cloud-app-manually). +- Required to [connect the GitLab for Jira Cloud app for self-managed instances](#connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances). +- Recommended to [install the GitLab for Jira Cloud app manually](#install-the-gitlab-for-jira-cloud-app-manually). To create an OAuth application: @@ -103,7 +103,7 @@ To create an OAuth application: 1. Select **Save changes**. 1. Optional. Enable the `jira_connect_oauth` [feature flag](../../administration/feature_flags.md) to avoid [authentication problems in some browsers](#browser-displays-a-sign-in-message-when-already-signed-in). -## Connect the GitLab.com for Jira Cloud app for self-managed instances **(FREE SELF)** +## Connect the GitLab for Jira Cloud app for self-managed instances **(FREE SELF)** > Introduced in GitLab 15.7. @@ -113,17 +113,17 @@ Prerequisites: - The instance must be publicly available. - The instance must be on version 15.7 or later. -You can link self-managed instances after installing the GitLab.com for Jira Cloud app from the marketplace. +You can link self-managed instances after installing the GitLab for Jira Cloud app from the marketplace. Jira apps can only link to one URL per marketplace listing. The official listing links to GitLab.com. If your instance doesn't meet the prerequisites or you don't want to use the official marketplace listing, you can -[install the app manually](#install-the-gitlabcom-for-jira-cloud-app-manually). +[install the app manually](#install-the-gitlab-for-jira-cloud-app-manually). It's not possible to create branches from Jira for self-managed instances. ### Set up your instance -To set up your self-managed instance for the GitLab.com for Jira Cloud app in GitLab 15.7 and later: +To set up your self-managed instance for the GitLab for Jira Cloud app in GitLab 15.7 and later: 1. [Set up OAuth authentication](#set-up-oauth-authentication). 1. On the top bar, select **Main menu > Admin**. @@ -134,14 +134,14 @@ To set up your self-managed instance for the GitLab.com for Jira Cloud app in Gi ### Link your instance -To link your self-managed instance to the GitLab.com for Jira Cloud app: +To link your self-managed instance to the GitLab for Jira Cloud app: -1. Install the [GitLab.com for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?tab=overview&hosting=cloud). +1. Install the [GitLab for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?tab=overview&hosting=cloud). 1. Select **GitLab (self-managed)**. 1. Enter your GitLab instance URL. 1. Select **Save**. -## Install the GitLab.com for Jira Cloud app manually **(FREE SELF)** +## Install the GitLab for Jira Cloud app manually **(FREE SELF)** If your GitLab instance is self-managed and you don't want to use the official marketplace listing, you can install the app manually. @@ -187,7 +187,7 @@ from outside the Marketplace, which allows you to install the application: 1. Disable [development mode](https://developer.atlassian.com/cloud/jira/platform/getting-started-with-connect/#step-2--enable-development-mode) on your Jira instance. -The **GitLab.com for Jira Cloud** app now displays under **Manage apps**. You can also +The **GitLab for Jira Cloud** app now displays under **Manage apps**. You can also select **Get started** to open the configuration page rendered from your GitLab instance. NOTE: @@ -211,7 +211,7 @@ To create a Marketplace listing: 1. Generate test license tokens for your application. NOTE: -This method uses [automated updates](#update-the-gitlabcom-for-jira-cloud-app) +This method uses [automated updates](#update-the-gitlab-for-jira-cloud-app) the same way as our GitLab.com Marketplace listing. ## Troubleshooting @@ -225,14 +225,14 @@ when you're already signed in: You need to sign in or sign up before continuing. ``` -The GitLab.com for Jira Cloud app uses an iframe to add namespaces on the +The GitLab for Jira Cloud app uses an iframe to add namespaces on the settings page. Some browsers block cross-site cookies, which can lead to this issue. To resolve this issue, set up [OAuth authentication](#set-up-oauth-authentication) and enable the `jira_connect_oauth` [feature flag](../../administration/feature_flags.md). ### Manual installation fails -You might get an error if you have installed the GitLab.com for Jira Cloud app from the official marketplace listing and replaced it with manual installation. To resolve this issue, disable the **Jira Connect Proxy URL** setting. +You might get an error if you have installed the GitLab for Jira Cloud app from the official marketplace listing and replaced it with manual installation. To resolve this issue, disable the **Jira Connect Proxy URL** setting. - In GitLab 15.7: diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md index 8fee11f8509..f72b714550c 100644 --- a/doc/integration/jira/development_panel.md +++ b/doc/integration/jira/development_panel.md @@ -69,7 +69,7 @@ To simplify administration, we recommend that a GitLab group maintainer or group | Jira usage | GitLab.com customers need | GitLab self-managed customers need | |------------|---------------------------|------------------------------------| -| [Atlassian cloud](https://www.atlassian.com/migration/assess/why-cloud) | The [GitLab.com for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) from the [Atlassian Marketplace](https://marketplace.atlassian.com). This method offers real-time sync between GitLab.com and Jira. For more information, see [GitLab.com for Jira Cloud app](connect-app.md). | The GitLab.com for Jira Cloud app [installed manually](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-manually). By default, you can install the app from the [Atlassian Marketplace](https://marketplace.atlassian.com/). For more information, see [Connect the GitLab.com for Jira Cloud app for self-managed instances](connect-app.md#connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances). | +| [Atlassian cloud](https://www.atlassian.com/migration/assess/why-cloud) | The [GitLab for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) from the [Atlassian Marketplace](https://marketplace.atlassian.com). This method offers real-time sync between GitLab.com and Jira. For more information, see [GitLab for Jira Cloud app](connect-app.md). | The GitLab for Jira Cloud app [installed manually](connect-app.md#install-the-gitlab-for-jira-cloud-app-manually). By default, you can install the app from the [Atlassian Marketplace](https://marketplace.atlassian.com/). For more information, see [Connect the GitLab for Jira Cloud app for self-managed instances](connect-app.md#connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances). | | Your own server | The [Jira DVCS (distributed version control system) connector](dvcs/index.md). This syncs data hourly. | The [Jira DVCS (distributed version control system) connector](dvcs/index.md). This syncs data hourly. | Each GitLab project can be configured to connect to an entire Jira instance. That means after diff --git a/doc/integration/jira/dvcs/index.md b/doc/integration/jira/dvcs/index.md index 1fa96e20d01..6659c0163cc 100644 --- a/doc/integration/jira/dvcs/index.md +++ b/doc/integration/jira/dvcs/index.md @@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w Use the Jira DVCS (distributed version control system) connector if you self-host your Jira instance, and you want to sync information between GitLab and Jira. If you use Jira Cloud, you should use the -[GitLab.com for Jira Cloud app](../connect-app.md) unless you specifically need the +[GitLab for Jira Cloud app](../connect-app.md) unless you specifically need the DVCS connector. When you configure the Jira DVCS connector, make sure your GitLab and Jira instances diff --git a/doc/integration/jira/index.md b/doc/integration/jira/index.md index 9307eeb5b5f..0bec7e7cec9 100644 --- a/doc/integration/jira/index.md +++ b/doc/integration/jira/index.md @@ -35,7 +35,7 @@ connects all GitLab projects under a group or personal namespace. When configure relevant GitLab information, including related branches, commits, and merge requests, displays in the [development panel](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/). -To set up the Jira development panel integration, use the GitLab.com for Jira Cloud app +To set up the Jira development panel integration, use the GitLab for Jira Cloud app or the Jira DVCS (distributed version control system) connector, [depending on your installation](development_panel.md#configure-the-integration). @@ -73,7 +73,7 @@ If you integrate a private GitLab project with Jira, the private data is shared with users who have access to your Jira project. The [**Jira project integration**](#jira-integration) posts GitLab data in the form of comments in Jira issues. -The GitLab.com for Jira Cloud app and Jira DVCS connector share this data through the [**Jira Development Panel**](development_panel.md). +The GitLab for Jira Cloud app and Jira DVCS connector share this data through the [**Jira Development Panel**](development_panel.md). This method provides more fine-grained access control because access can be restricted to certain user groups or roles. ## Third-party Jira integrations diff --git a/doc/operations/incident_management/alerts.md b/doc/operations/incident_management/alerts.md index d42ee237749..6d6fde45de7 100644 --- a/doc/operations/incident_management/alerts.md +++ b/doc/operations/incident_management/alerts.md @@ -35,7 +35,7 @@ The alert list displays the following information: - **Triggered**: Investigation has not started. - **Acknowledged**: Someone is actively investigating the problem. - **Resolved**: No further work is required. - - **Ignored**: No action will be taken on the alert. + - **Ignored**: No action is taken on the alert. ## Alert severity diff --git a/doc/operations/metrics/dashboards/index.md b/doc/operations/metrics/dashboards/index.md index aef9bcb4b22..9d5641bd800 100644 --- a/doc/operations/metrics/dashboards/index.md +++ b/doc/operations/metrics/dashboards/index.md @@ -190,7 +190,7 @@ Related links can contain the following attributes: - `title`: A phrase describing the link. Optional. If this attribute is not set, the full URL is used for the link title. - `type`: A string declaring the type of link. Optional. If set to `grafana`, the - dashboard's time range values are converted to Grafana's time range format and + dashboard's time range values are converted to the Grafana time range format and appended to the `url`. The dashboard's time range is appended to the `url` as URL parameters. diff --git a/doc/subscriptions/quarterly_reconciliation.md b/doc/subscriptions/quarterly_reconciliation.md index 508dd2924e3..2b45d145aaf 100644 --- a/doc/subscriptions/quarterly_reconciliation.md +++ b/doc/subscriptions/quarterly_reconciliation.md @@ -34,12 +34,12 @@ and for only the remaining quarters. Using the same example, if a seat is $100 per year, then it is $25 per quarter. -- In Q1, you had a maximum of 110 users. 10 users over license x $25 per user x 3 quarters = **$750** +- In Q1, you had a maximum of 110 users. 10 users over subscription x $25 per user x 3 quarters = **$750** The license is now paid for 110 users. - In Q2, 105 users was the maximum. You did not go over 110 users, so no charge. -- In Q3, you had 120 users. 10 users over license x $25 per user x 1 remaining quarter = **$250** +- In Q3, you had 120 users. 10 users over subscription x $25 per user x 1 remaining quarter = **$250** The license is now paid for 120 users. - In Q4, you had 120 users. You did not exceed the number of users. However, if you had, you would not be charged, because in Q4, there are no charges for exceeding the number. @@ -71,7 +71,7 @@ sent and subject to your terms. ### Self-managed instances Administrators receive an email **six days after the reconciliation date**. -This email communicates the [overage seat quantity](self_managed/index.md#users-over-license) +This email communicates the [overage seat quantity](self_managed/index.md#users-over-subscription) and expected invoice amount. **Seven days later**, the subscription is updated to include the additional diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md index 389bad87844..e3d6630c85b 100644 --- a/doc/subscriptions/self_managed/index.md +++ b/doc/subscriptions/self_managed/index.md @@ -46,7 +46,7 @@ according to the [maximum number](#maximum-users) of users enabled during the su For instances that aren't offline or on a closed network, the maximum number of simultaneous users in the GitLab self-managed installation is checked each quarter. -If an instance is unable to generate a quarterly usage report, the existing [true up model](#users-over-license) is used. +If an instance is unable to generate a quarterly usage report, the existing [true up model](#users-over-subscription) is used. Prorated charges are not possible without a quarterly usage report. ### View user totals @@ -82,20 +82,20 @@ billable user, with the following exceptions: The number of _maximum users_ reflects the highest number of billable users for the current license period. -#### Users over license +#### Users over subscription -The number of _users over license_ shows how many users are in excess of the number allowed by the license. This number reflects the current license period. +The number of _users over subscription_ shows how many users are in excess of the number allowed by the subscription. This number reflects the current subscription period. For example, if: -- The license allows 100 users and +- The subscription allows 100 users and - **Maximum users** is 150, Then this value would be 50. If the **Maximum users** value is less than or equal to 100, then this value is 0. -A trial license always displays zero for **Users over license**. +A trial license always displays zero for **Users over subscription**. If you add more users to your GitLab instance than you are licensed for, payment for the additional users is due [at the time of renewal](../quarterly_reconciliation.md). @@ -269,7 +269,7 @@ It also displays the following information: | Users in License | The number of users you've paid for in the current license loaded on the system. The number does not change unless you [add seats](#add-seats-to-a-subscription) during your current subscription period. | | Billable users | The daily count of billable users on your system. The count may change as you block, deactivate, or add users to your instance. | | Maximum users | The highest number of billable users on your system during the term of the loaded license. | -| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that must be paid before renewal. | +| Users over subscription | Calculated as `Maximum users` - `Users in subscription` for the current license term. This number incurs a retroactive charge that must be paid before renewal. | ## Export your license usage @@ -327,18 +327,18 @@ It's important to regularly review your user accounts, because: if you renew for too many users. - Stale user accounts can be a security risk. A regular review helps reduce this risk. -#### Users over License +#### Users over subscription -A GitLab subscription is valid for a specific number of seats. The number of users over license -is the number of _Maximum users_ that exceed the _Users in License_ for the current license term. +A GitLab subscription is valid for a specific number of seats. The number of users over subscription +is the number of _maximum users_ that exceed the users in subscription for the current subscription term. You must pay for this number of users either before renewal, or at the time of renewal. This is -known as the _true up_ process. +called the _true up_ process. -To view the number of _users over license_ go to the **Admin Area**. +To view the number of users over subscription go to the **Admin Area**. -##### Users over license example +##### Users over subscription example -You purchase a license for 10 users. +You purchase a subscription for 10 users. | Event | Billable users | Maximum users | |:---------------------------------------------------|:-----------------|:--------------| @@ -346,7 +346,7 @@ You purchase a license for 10 users. | Two new users join. | 12 | 12 | | Three users leave and their accounts are removed. | 9 | 12 | -Users over license = 12 - 10 (Maximum users - users in license) +Users over subscription = 12 - 10 (Maximum users - users in license) ### Add seats to a subscription @@ -387,7 +387,7 @@ You can hover your mouse on the **Renew** button to see the date when it will be If you need to change your [GitLab tier](https://about.gitlab.com/pricing/), contact our sales team with [the sales contact form](https://about.gitlab.com/sales/) for assistance as this can't be done in the Customers Portal. 1. In the first box, enter the total number of user licenses you'll need for the upcoming year. Be sure this number is at least **equal to, or greater than** the number of billable users in the system at the time of performing the renewal. -1. Enter the number of [users over license](#users-over-license) in the second box for the user overage incurred in your previous subscription term. +1. Enter the number of [users over subscription](#users-over-subscription) in the second box for the user overage incurred in your previous subscription term. 1. Review your renewal details and complete the payment process. 1. An activation code for the renewal term is available on the [Manage Purchases](https://customers.gitlab.com/subscriptions) page on the relevant subscription card. Select **Copy activation code** to get a copy. 1. [Add the activation code](../../user/admin_area/license.md) to your instance. diff --git a/doc/user/application_security/dast/authentication.md b/doc/user/application_security/dast/authentication.md index de3bdad3c42..77732ab532c 100644 --- a/doc/user/application_security/dast/authentication.md +++ b/doc/user/application_security/dast/authentication.md @@ -48,8 +48,8 @@ To run a DAST authenticated scan: - You are using either the [DAST proxy-based analyzer](proxy-based.md) or the [DAST browser-based analyzer](browser_based.md). - You know the URL of the login form of your application. Alternatively, you know how to navigate to the login form from the authentication URL (see [clicking to navigate to the login form](#clicking-to-navigate-to-the-login-form)). - You have the username and password of the user you would like to authenticate as during the scan. -- You know the [selectors](#finding-an-elements-selector) of the username and password HTML fields that DAST will use to input the respective values. -- You know the element's [selector](#finding-an-elements-selector) that will submit the login form when selected. +- You know the [selectors](#finding-an-elements-selector) of the username and password HTML fields that DAST uses to input the respective values. +- You know the element's [selector](#finding-an-elements-selector) that submits the login form when selected. - You have thought about how you can [verify](#verifying-authentication-is-successful) whether or not authentication was successful. - You have checked the [known limitations](#known-limitations) to ensure DAST can authenticate to your application. @@ -144,7 +144,7 @@ See [Custom CI/CI variables](../../../ci/variables/index.md#for-a-project) for m ### Configuration for Single Sign-On (SSO) -If a user can log into an application, then in most cases, DAST will also be able to log in. +If a user can log into an application, then in most cases, DAST is also able to log in. This is the case even when an application uses Single Sign-on. Applications using SSO solutions should configure DAST authentication using the [single-step](#configuration-for-a-single-step-login-form) or [multi-step](#configuration-for-a-multi-step-login-form) login form configuration guides. @@ -172,8 +172,8 @@ dast: ### Excluding logout URLs -If DAST crawls the logout URL while running an authenticated scan, the user will be logged out, resulting in the remainder of the scan being unauthenticated. -It is therefore recommended to exclude logout URLs using the CI/CD variable `DAST_EXCLUDE_URLS`. DAST will not access any excluded URLs, ensuring the user remains logged in. +If DAST crawls the logout URL while running an authenticated scan, the user is logged out, resulting in the remainder of the scan being unauthenticated. +It is therefore recommended to exclude logout URLs using the CI/CD variable `DAST_EXCLUDE_URLS`. DAST isn't accessing any excluded URLs, ensuring the user remains logged in. Provided URLs can be either absolute URLs, or regular expressions of URL paths relative to the base path of the `DAST_WEBSITE`. For example: @@ -197,7 +197,7 @@ Selectors have the format `type`:`search string`. DAST searches for the selector | `css` | `css:.password-field` | Searches for a HTML element having the supplied CSS selector. Selectors should be as specific as possible for performance reasons. | | `id` | `id:element` | Searches for an HTML element with the provided element ID. | | `name` | `name:element` | Searches for an HTML element with the provided element name. | -| `xpath` | `xpath://input[@id="my-button"]/a` | Searches for a HTML element with the provided XPath. Note that XPath searches are expected to be less performant than other searches. | +| `xpath` | `xpath://input[@id="my-button"]/a` | Searches for a HTML element with the provided XPath. XPath searches are expected to be less performant than other searches. | | None provided | `a.click-me` | Defaults to searching using a CSS selector. **{warning}** **[Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/383348)** in GitLab 15.8. Replaced by explicitly declaring the selector type. | #### Find selectors with Google Chrome @@ -238,7 +238,7 @@ When using selectors to locate specific fields we recommend you avoid searching ## Verifying authentication is successful Once DAST has submitted the login form, a verification process takes place -to determine if authentication succeeded. The scan will halt with an error if authentication is unsuccessful. +to determine if authentication succeeded. The scan halts with an error if authentication is unsuccessful. Following the submission of the login form, authentication is determined to be unsuccessful when: @@ -251,13 +251,13 @@ Following the submission of the login form, authentication is determined to be u Verification checks run checks on the state of the browser once authentication is complete to determine further if authentication succeeded. -DAST will test for the absence of a login form if no verification checks are configured. +DAST tests for the absence of a login form if no verification checks are configured. #### Verify based on the URL Define `DAST_AUTH_VERIFICATION_URL` as the URL displayed in the browser tab once the login form is successfully submitted. -DAST will compare the verification URL to the URL in the browser after authentication. +DAST compares the verification URL to the URL in the browser after authentication. If they are not the same, authentication is unsuccessful. For example: @@ -274,7 +274,7 @@ dast: #### Verify based on presence of an element -Define `DAST_AUTH_VERIFICATION_SELECTOR` as a [selector](#finding-an-elements-selector) that will find one or many elements on the page +Define `DAST_AUTH_VERIFICATION_SELECTOR` as a [selector](#finding-an-elements-selector) that finds one or many elements on the page displayed once the login form is successfully submitted. If no element is found, authentication is unsuccessful. Searching for the selector on the page displayed when login fails should return no elements. @@ -337,8 +337,8 @@ dast: ## Known limitations -- DAST cannot bypass a CAPTCHA if the authentication flow includes one. Please turn these off in the testing environment for the application being scanned. -- DAST cannot handle multi-factor authentication like one-time passwords (OTP) by using SMS, biometrics, or authenticator apps. Please turn these off in the testing environment for the application being scanned. +- DAST cannot bypass a CAPTCHA if the authentication flow includes one. Turn these off in the testing environment for the application being scanned. +- DAST cannot handle multi-factor authentication like one-time passwords (OTP) by using SMS, biometrics, or authenticator apps. Turn these off in the testing environment for the application being scanned. - DAST cannot authenticate to applications that do not set an [authentication token](#authentication-tokens) during login. - DAST cannot authenticate to applications that require more than two inputs to be filled out. Two inputs must be supplied, username and password. diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index 8ae3715f3bf..9de9d445965 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -15,6 +15,15 @@ files or directories in a repository. - You can set your merge requests so they must be approved by Code Owners before merge. - You can protect a branch and allow only Code Owners to approve changes to the branch. +<div class="video-fallback"> + Video introduction: <a href="https://www.youtube.com/watch?v=RoyBySTUSB0">Code Owners</a>. +</div> +<figure class="video-container"> + <iframe src="https://www.youtube-nocookie.com/embed/RoyBySTUSB0" frameborder="0" allowfullscreen> </iframe> +</figure> + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> + Use Code Owners and approvers together with [approval rules](merge_requests/approvals/rules.md) to build a flexible approval workflow: diff --git a/lefthook.yml b/lefthook.yml index d62a90d150e..39496e0d241 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -14,7 +14,7 @@ pre-push: tags: style files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD glob: '*.{json}' - run: scripts/lint-json.sh {files} + run: scripts/lint-json -v {files} haml-lint: tags: view haml style files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD diff --git a/lib/gitlab/ci/interpolation/access.rb b/lib/gitlab/ci/interpolation/access.rb new file mode 100644 index 00000000000..42598458902 --- /dev/null +++ b/lib/gitlab/ci/interpolation/access.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + class Access + attr_reader :content, :errors + + MAX_ACCESS_OBJECTS = 5 + MAX_ACCESS_BYTESIZE = 1024 + + def initialize(access, ctx) + @content = access + @ctx = ctx + @errors = [] + + if objects.count <= 1 # rubocop:disable Style/IfUnlessModifier + @errors.push('invalid interpolation access pattern') + end + + if access.bytesize > MAX_ACCESS_BYTESIZE # rubocop:disable Style/IfUnlessModifier + @errors.push('maximum interpolation expression size exceeded') + end + + evaluate! if valid? + end + + def valid? + errors.none? + end + + def objects + @objects ||= @content.split('.', MAX_ACCESS_OBJECTS) + end + + def value + raise ArgumentError, 'access path invalid' unless valid? + + @value + end + + private + + def evaluate! + raise ArgumentError, 'access path invalid' unless valid? + + @value ||= objects.inject(@ctx) do |memo, value| + memo.fetch(value.to_sym) + end + rescue KeyError => e + @errors.push(e) + end + end + end + end +end diff --git a/lib/gitlab/ci/interpolation/block.rb b/lib/gitlab/ci/interpolation/block.rb new file mode 100644 index 00000000000..389cbf378a2 --- /dev/null +++ b/lib/gitlab/ci/interpolation/block.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + class Block + PREFIX = '$[[' + PATTERN = /(?<block>\$\[\[\s*(?<access>.*?)\s*\]\])/.freeze + + attr_reader :block, :data, :ctx + + def initialize(block, data, ctx) + @block = block + @ctx = ctx + @data = data + + @access = Interpolation::Access.new(@data, ctx) + end + + def valid? + errors.none? + end + + def errors + @access.errors + end + + def content + @access.content + end + + def value + raise ArgumentError, 'block invalid' unless valid? + + @access.value + end + + def self.match(data) + return data unless data.is_a?(String) && data.include?(PREFIX) + + data.gsub(PATTERN) do + yield ::Regexp.last_match(1), ::Regexp.last_match(2) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/interpolation/config.rb b/lib/gitlab/ci/interpolation/config.rb new file mode 100644 index 00000000000..32f58521139 --- /dev/null +++ b/lib/gitlab/ci/interpolation/config.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + ## + # Interpolation::Config represents a configuration artifact that we want to perform interpolation on. + # + class Config + include Gitlab::Utils::StrongMemoize + ## + # Total number of hash nodes traversed. For example, loading a YAML below would result in a hash having 12 nodes + # instead of 9, because hash values are being counted before we recursively traverse them. + # + # test: + # spec: + # env: $[[ inputs.env ]] + # + # $[[ inputs.key ]]: + # name: $[[ inputs.key ]] + # script: my-value + # + # According to our benchmarks performed when developing this code, the worst-case scenario of processing + # a hash with 500_000 nodes takes around 1 second and consumes around 225 megabytes of memory. + # + # The typical scenario, using just a few interpolations takes 250ms and consumes around 20 megabytes of memory. + # + # Given the above the 500_000 nodes should be an upper limit, provided that the are additional safeguard + # present in other parts of the code (example: maximum number of interpolation blocks found). Typical size of a + # YAML configuration with 500k nodes might be around 10 megabytes, which is an order of magnitude higher than + # the 1MB limit for loading YAML on GitLab.com + # + MAX_NODES = 500_000 + MAX_NODE_SIZE = 1024 * 1024 # 1MB + + TooManyNodesError = Class.new(StandardError) + NodeTooLargeError = Class.new(StandardError) + + Visitor = Class.new do + def initialize + @visited = 0 + end + + def visit! + @visited += 1 + + raise Config::TooManyNodesError if @visited > Config::MAX_NODES + end + end + + attr_reader :errors + + def initialize(hash) + @config = hash + @errors = [] + end + + def to_h + @config + end + + ## + # The replace! method will yield a block and replace a each of the hash config nodes with a return value of the + # block. + # + # It returns `nil` if there were errors found during the process. + # + def replace!(&block) + recursive_replace(@config, Visitor.new, &block) + rescue TooManyNodesError + @errors.push('config too large') + nil + rescue NodeTooLargeError + @errors.push('config node too large') + nil + end + strong_memoize_attr :replace! + + def self.fabricate(config) + case config + when Hash + new(config) + when Interpolation::Config + config + else + raise ArgumentError, 'unknown interpolation config' + end + end + + private + + def recursive_replace(config, visitor, &block) + visitor.visit! + + case config + when Hash + {}.tap do |new_hash| + config.each_pair do |key, value| + new_key = recursive_replace(key, visitor, &block) + new_value = recursive_replace(value, visitor, &block) + + if new_key != key + new_hash[new_key] = new_value + else + new_hash[key] = new_value + end + end + end + when Array + config.map { |value| recursive_replace(value, visitor, &block) } + when Symbol + recursive_replace(config.to_s, visitor, &block) + when String + raise NodeTooLargeError if config.bytesize > MAX_NODE_SIZE + + yield config + else + config + end + end + end + end + end +end diff --git a/lib/gitlab/ci/interpolation/context.rb b/lib/gitlab/ci/interpolation/context.rb new file mode 100644 index 00000000000..ce7a86a3c9b --- /dev/null +++ b/lib/gitlab/ci/interpolation/context.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + ## + # Interpolation::Context is a class that represents the data that can be used when performing string interpolation + # on a CI configuration. + # + class Context + ContextTooComplexError = Class.new(StandardError) + NotSymbolizedContextError = Class.new(StandardError) + + MAX_DEPTH = 3 + + def initialize(hash) + @context = hash + + raise ContextTooComplexError if depth > MAX_DEPTH + end + + def valid? + errors.none? + end + + ## + # This method is here because `Context` will be responsible for validating specs, inputs and defaults. + # + def errors + [] + end + + def depth + deep_depth(@context) + end + + def fetch(field) + @context.fetch(field) + end + + def to_h + @context.to_h + end + + private + + def deep_depth(context, depth = 0) + values = context.values.map do |value| + if value.is_a?(Hash) + deep_depth(value, depth + 1) + else + depth + 1 + end + end + + values.max + end + + def self.fabricate(context) + case context + when Hash + new(context) + when Interpolation::Context + context + else + raise ArgumentError, 'unknown interpolation context' + end + end + end + end + end +end diff --git a/lib/gitlab/ci/interpolation/template.rb b/lib/gitlab/ci/interpolation/template.rb new file mode 100644 index 00000000000..0211279f266 --- /dev/null +++ b/lib/gitlab/ci/interpolation/template.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + class Template + include Gitlab::Utils::StrongMemoize + + attr_reader :blocks, :ctx + + TooManyBlocksError = Class.new(StandardError) + InvalidBlockError = Class.new(StandardError) + + MAX_BLOCKS = 10_000 + + def initialize(config, ctx) + @config = Interpolation::Config.fabricate(config) + @ctx = Interpolation::Context.fabricate(ctx) + @errors = [] + @blocks = {} + + interpolate! if valid? + end + + def valid? + errors.none? + end + + def errors + @errors + @config.errors + @ctx.errors + @blocks.values.flat_map(&:errors) + end + + def size + @blocks.size + end + + def interpolated + @result if valid? + end + + private + + def interpolate! + @result = @config.replace! do |data| + Interpolation::Block.match(data) do |block, data| + evaluate_block(block, data) + end + end + rescue TooManyBlocksError + @errors.push('too many interpolation blocks') + rescue InvalidBlockError + @errors.push('interpolation interrupted by errors') + end + strong_memoize_attr :interpolate! + + def evaluate_block(block, data) + block = (@blocks[block] ||= Interpolation::Block.new(block, data, ctx)) + + raise TooManyBlocksError if @blocks.count > MAX_BLOCKS + raise InvalidBlockError unless block.valid? + + block.value + end + end + end + end +end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index ceef072a710..7abe0351a39 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -69,6 +69,7 @@ module Gitlab push_frontend_feature_flag(:vscode_web_ide, current_user) push_frontend_feature_flag(:integration_slack_app_notifications) push_frontend_feature_flag(:new_fonts, current_user) + push_frontend_feature_flag(:full_path_project_search, current_user) end # Exposes the state of a feature flag to the frontend code. diff --git a/package.json b/package.json index f477b8407a6..0c029a83e6d 100644 --- a/package.json +++ b/package.json @@ -247,7 +247,6 @@ "jest-jasmine2": "^28.1.3", "jest-junit": "^12.3.0", "jest-util": "^28.1.3", - "jsonlint": "^1.6.3", "markdownlint-cli": "0.32.2", "miragejs": "^0.1.40", "mock-apollo-client": "1.2.0", diff --git a/qa/Gemfile b/qa/Gemfile index cdbffa060ef..075932e3f25 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -gem 'gitlab-qa', '~> 8', '>= 8.15.1', require: 'gitlab/qa' +gem 'gitlab-qa', '~> 8', '>= 8.15.2', require: 'gitlab/qa' gem 'activesupport', '~> 6.1.7.1' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.20.0' gem 'capybara', '~> 3.38.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index ecd487e8bb3..458145a494c 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -102,7 +102,7 @@ GEM gitlab (4.18.0) httparty (~> 0.18) terminal-table (>= 1.5.1) - gitlab-qa (8.15.1) + gitlab-qa (8.15.2) activesupport (~> 6.1) gitlab (~> 4.18.0) http (~> 5.0) @@ -317,7 +317,7 @@ DEPENDENCIES faraday-retry (~> 2.0) fog-core (= 2.1.0) fog-google (~> 1.19) - gitlab-qa (~> 8, >= 8.15.1) + gitlab-qa (~> 8, >= 8.15.2) influxdb-client (~> 2.9) knapsack (~> 4.0) nokogiri (~> 1.14) @@ -342,4 +342,4 @@ DEPENDENCIES zeitwerk (~> 2.6, >= 2.6.6) BUNDLED WITH - 2.4.4 + 2.4.5 diff --git a/scripts/lint-json b/scripts/lint-json new file mode 100755 index 00000000000..3fa952b13df --- /dev/null +++ b/scripts/lint-json @@ -0,0 +1,77 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "json" +require "optparse" +require "rainbow/refinement" +using Rainbow + +options = {} + +OptionParser.new do |opts| + opts.banner = 'Checks if JSON files are pretty.' + + opts.on('-f', '--format', 'Format JSON files inline.') do + options[:format] = true + end + + opts.on('-s', '--stats', 'Print statistics after processing.') do + options[:stats] = true + end + + opts.on('-v', '--verbose', 'Increase verbosity.') do + options[:verbose] = true + end + + opts.on('-q', '--quiet', 'Do not print anything. Disables -s and -v') do + options[:quiet] = true + end + + opts.on('-h', '--help', 'Prints this help') do + abort opts.to_s + end +end.parse! + +def make_pretty(file, format:, verbose:, quiet:) + json = File.read(file) + pretty = JSON.pretty_generate(JSON.parse(json)) << "\n" + + return :pretty if json == pretty + + puts "#{file} is not pretty" if verbose && !quiet + return :todo unless format + + puts "#{file} was not pretty. Fixed!" unless quiet + File.write(file, pretty) + :formatted +rescue JSON::ParserError + puts "#{file} is invalid. Skipping!" unless quiet + :error +end + +results = ARGV + .lazy + .flat_map { |pattern| Dir.glob(pattern) } + .map { |file| make_pretty(file, format: options[:format], verbose: options[:verbose], quiet: options[:quiet]) } + .to_a + +if options[:stats] && !options[:quiet] + puts format("Scanned total=%<total>d, pretty=%<pretty>d, formatted=%<formatted>d, error=%<error>d", + total: results.size, + pretty: results.count { |result| result == :pretty }, + formatted: results.count { |result| result == :formatted }, + error: results.count { |result| result == :error } + ) +end + +if results.any?(:todo) + unless options[:quiet] + puts "\nSome of the JSON files are not pretty-printed, you can run:".yellow + puts "\tscripts/lint-json -f $(git diff --name-only master... | grep \\\\.json)".white + puts "to fix them".yellow + end + + exit(1) +else + exit(0) +end diff --git a/scripts/lint-json.sh b/scripts/lint-json.sh deleted file mode 100755 index 685661c789a..00000000000 --- a/scripts/lint-json.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -for file in "$@" -do - yarn run -s jsonlint -p "$file" | perl -pe 'chomp if eof' | diff "$file" - -done diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 393cd6f6a21..e53f0cd936f 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -18,6 +18,8 @@ require 'active_support/dependencies' require_relative '../config/initializers/0_inject_enterprise_edition_module' require_relative '../config/settings' require_relative 'support/rspec' +require_relative '../lib/gitlab/utils' +require_relative '../lib/gitlab/utils/strong_memoize' require 'active_support/all' require_relative 'simplecov_env' diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index e6630e40147..363c152371e 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -105,8 +105,8 @@ RSpec.describe 'Admin::Hooks', feature_category: :integrations do WebMock.stub_request(:post, system_hook.url) visit admin_hooks_path - find('.hook-test-button.dropdown').click - click_link 'Push events' + click_button 'Test' + click_button 'Push events' end it { expect(page).to have_current_path(admin_hooks_path, ignore_query: true) } @@ -141,8 +141,8 @@ RSpec.describe 'Admin::Hooks', feature_category: :integrations do create(:merge_request, source_project: project) visit admin_hooks_path - find('.hook-test-button.dropdown').click - click_link 'Merge request events' + click_button 'Test' + click_button 'Merge request events' expect(page).to have_content 'Hook executed successfully' end diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb index 8d22d84b9c9..3b8b982b621 100644 --- a/spec/features/projects/settings/webhooks_settings_spec.rb +++ b/spec/features/projects/settings/webhooks_settings_spec.rb @@ -82,8 +82,8 @@ RSpec.describe 'Projects > Settings > Webhook Settings', feature_category: :proj WebMock.stub_request(:post, hook.url) visit webhooks_path - find('.hook-test-button.dropdown').click - click_link 'Push events' + click_button 'Test' + click_button 'Push events' expect(page).to have_current_path(webhooks_path, ignore_query: true) end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index 7462b108027..4a5eb389906 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -12,9 +12,9 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do let_it_be(:user2) { create(:user) } let_it_be(:user3) { create(:user) } let_it_be(:user4) { create(:user) } - let_it_be(:user5) { create(:user, :two_factor_via_otp) } + let_it_be(:user5_2fa) { create(:user, :two_factor_via_otp) } - let!(:link) do + let_it_be(:link) do create(:group_group_link, shared_group: group, shared_with_group: public_shared_group) create(:group_group_link, shared_group: sub_group, shared_with_group: private_shared_group) end @@ -30,7 +30,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do end context 'relations' do - let!(:members) do + let_it_be(:members) do { user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1), user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1), @@ -52,7 +52,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now), user4_public_shared_group: create(:group_member, :developer, group: public_shared_group, user: user4), user4_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user4), - user5_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user5) + user5_private_shared_group: create(:group_member, :developer, group: private_shared_group, user: user5_2fa) } end @@ -98,35 +98,31 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do end context 'search' do - it 'returns searched members if requested' do + before_all do group.add_maintainer(user2) group.add_developer(user3) - member = group.add_maintainer(user1) + end + + let_it_be(:maintainer1) { group.add_maintainer(user1) } + it 'returns searched members if requested' do result = described_class.new(group, params: { search: user1.name }).execute - expect(result.to_a).to match_array([member]) + expect(result.to_a).to match_array([maintainer1]) end it 'returns nothing if search only in inherited relation' do - group.add_maintainer(user2) - group.add_developer(user3) - group.add_maintainer(user1) - result = described_class.new(group, params: { search: user1.name }).execute(include_relations: [:inherited]) expect(result.to_a).to match_array([]) end it 'returns searched member only from sub_group if search only in inherited relation' do - group.add_maintainer(user2) - group.add_developer(user3) sub_group.add_maintainer(create(:user, name: user1.name)) - member = group.add_maintainer(user1) - result = described_class.new(sub_group, params: { search: member.user.name }).execute(include_relations: [:inherited]) + result = described_class.new(sub_group, params: { search: maintainer1.user.name }).execute(include_relations: [:inherited]) - expect(result.to_a).to contain_exactly(member) + expect(result.to_a).to contain_exactly(maintainer1) end end @@ -134,7 +130,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do it 'returns members with two-factor auth if requested by owner' do group.add_owner(user2) group.add_maintainer(user1) - member = group.add_maintainer(user5) + member = group.add_maintainer(user5_2fa) result = described_class.new(group, user2, params: { two_factor: 'enabled' }).execute @@ -144,7 +140,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do it 'returns members without two-factor auth if requested by owner' do member1 = group.add_owner(user2) member2 = group.add_maintainer(user1) - member_with_2fa = group.add_maintainer(user5) + member_with_2fa = group.add_maintainer(user5_2fa) result = described_class.new(group, user2, params: { two_factor: 'disabled' }).execute @@ -156,7 +152,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do group.add_owner(user1) group.add_maintainer(user2) sub_group.add_maintainer(user3) - member_with_2fa = sub_group.add_maintainer(user5) + member_with_2fa = sub_group.add_maintainer(user5_2fa) result = described_class.new(sub_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:direct]) @@ -165,7 +161,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do it 'returns inherited members with two-factor auth if requested by owner' do group.add_owner(user1) - member_with_2fa = group.add_maintainer(user5) + member_with_2fa = group.add_maintainer(user5_2fa) sub_group.add_maintainer(user2) sub_group.add_maintainer(user3) @@ -178,7 +174,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do group.add_owner(user1) group.add_maintainer(user2) member3 = sub_group.add_maintainer(user3) - sub_group.add_maintainer(user5) + sub_group.add_maintainer(user5_2fa) result = described_class.new(sub_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:direct]) @@ -187,7 +183,7 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do it 'returns inherited members without two-factor auth if requested by owner' do member1 = group.add_owner(user1) - group.add_maintainer(user5) + group.add_maintainer(user5_2fa) sub_group.add_maintainer(user2) sub_group.add_maintainer(user3) @@ -198,10 +194,10 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :subgroups do end context 'filter by access levels' do - let!(:owner1) { group.add_owner(user2) } - let!(:owner2) { group.add_owner(user3) } - let!(:maintainer1) { group.add_maintainer(user4) } - let!(:maintainer2) { group.add_maintainer(user5) } + let_it_be(:owner1) { group.add_owner(user2) } + let_it_be(:owner2) { group.add_owner(user3) } + let_it_be(:maintainer1) { group.add_maintainer(user4) } + let_it_be(:maintainer2) { group.add_maintainer(user5_2fa) } subject(:by_access_levels) { described_class.new(group, user1, params: { access_levels: access_levels }).execute } diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js index e2fd3b75fe9..649adfbcc56 100644 --- a/spec/frontend/api/projects_api_spec.js +++ b/spec/frontend/api/projects_api_spec.js @@ -10,12 +10,15 @@ describe('~/api/projects_api.js', () => { let originalGon; const projectId = 1; + const setfullPathProjectSearch = (value) => { + window.gon.features.fullPathProjectSearch = value; + }; beforeEach(() => { mock = new MockAdapter(axios); originalGon = window.gon; - window.gon = { api_version: 'v7' }; + window.gon = { api_version: 'v7', features: { fullPathProjectSearch: true } }; }); afterEach(() => { @@ -28,12 +31,51 @@ describe('~/api/projects_api.js', () => { jest.spyOn(axios, 'get'); }); + const expectedUrl = '/api/v7/projects.json'; + const expectedProjects = [{ name: 'project 1' }]; + const options = {}; + it('retrieves projects from the correct URL and returns them in the response data', () => { - const expectedUrl = '/api/v7/projects.json'; const expectedParams = { params: { per_page: 20, search: '', simple: true } }; - const expectedProjects = [{ name: 'project 1' }]; const query = ''; - const options = {}; + + mock.onGet(expectedUrl).reply(200, { data: expectedProjects }); + + return projectsApi.getProjects(query, options).then(({ data }) => { + expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams); + expect(data.data).toEqual(expectedProjects); + }); + }); + + it('omits search param if query is undefined', () => { + const expectedParams = { params: { per_page: 20, simple: true } }; + + mock.onGet(expectedUrl).reply(200, { data: expectedProjects }); + + return projectsApi.getProjects(undefined, options).then(({ data }) => { + expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams); + expect(data.data).toEqual(expectedProjects); + }); + }); + + it('searches namespaces if query contains a slash', () => { + const expectedParams = { + params: { per_page: 20, search: 'group/project1', search_namespaces: true, simple: true }, + }; + const query = 'group/project1'; + + mock.onGet(expectedUrl).reply(200, { data: expectedProjects }); + + return projectsApi.getProjects(query, options).then(({ data }) => { + expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams); + expect(data.data).toEqual(expectedProjects); + }); + }); + + it('does not search namespaces if fullPathProjectSearch is disabled', () => { + setfullPathProjectSearch(false); + const expectedParams = { params: { per_page: 20, search: 'group/project1', simple: true } }; + const query = 'group/project1'; mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects }); diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js index 77ffe37546e..e1890555de0 100644 --- a/spec/frontend/frequent_items/components/app_spec.js +++ b/spec/frontend/frequent_items/components/app_spec.js @@ -33,6 +33,7 @@ describe('Frequent Items App Component', () => { const createComponent = (props = {}) => { const session = currentSession[TEST_NAMESPACE]; gon.api_version = session.apiVersion; + gon.features = { fullPathProjectSearch: true }; wrapper = mountExtended(App, { store, diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js index 2feb488da2c..c228bca4973 100644 --- a/spec/frontend/frequent_items/store/actions_spec.js +++ b/spec/frontend/frequent_items/store/actions_spec.js @@ -25,6 +25,7 @@ describe('Frequent Items Dropdown Store Actions', () => { mockedState.namespace = mockNamespace; mockedState.storageKey = mockStorageKey; + gon.features = { fullPathProjectSearch: true }; }); afterEach(() => { diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 9fbb3d0a660..7aab1013fc0 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -192,9 +192,10 @@ describe('init markdown', () => { }); describe('Continuing markdown lists', () => { - const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); + let enterEvent; beforeEach(() => { + enterEvent = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true }); textArea.addEventListener('keydown', keypressNoteText); textArea.addEventListener('compositionstart', compositionStartNoteText); textArea.addEventListener('compositionend', compositionEndNoteText); @@ -256,7 +257,7 @@ describe('init markdown', () => { ${'108. item\n109. '} | ${'108. item\n'} ${'108. item\n - second\n - '} | ${'108. item\n - second\n'} ${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'} - `('adds correct list continuation characters', ({ text, expected }) => { + `('remove list continuation characters', ({ text, expected }) => { textArea.value = text; textArea.setSelectionRange(text.length, text.length); @@ -300,6 +301,37 @@ describe('init markdown', () => { }, ); + // test that when pressing Enter in the prefix area of a list item, + // such as between `2.`, we simply propagate the Enter, + // adding a newline. Since the event doesn't actually get propagated + // in the test, check that `defaultPrevented` is false + it.each` + text | add_at | prevented + ${'- one\n- two\n- three'} | ${6} | ${false} + ${'- one\n- two\n- three'} | ${7} | ${false} + ${'- one\n- two\n- three'} | ${8} | ${true} + ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${10} | ${false} + ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${15} | ${false} + ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${16} | ${true} + ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${10} | ${false} + ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${11} | ${false} + ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${17} | ${false} + ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${18} | ${true} + ${'1. one\n2. two\n3. three'} | ${7} | ${false} + ${'1. one\n2. two\n3. three'} | ${9} | ${false} + ${'1. one\n2. two\n3. three'} | ${10} | ${true} + `( + 'allows a newline to be added if cursor is inside the list marker prefix area', + ({ text, add_at, prevented }) => { + textArea.value = text; + textArea.setSelectionRange(add_at, add_at); + + textArea.dispatchEvent(enterEvent); + + expect(enterEvent.defaultPrevented).toBe(prevented); + }, + ); + it('does not duplicate a line item for IME characters', () => { const text = '- 日本語'; const expected = '- 日本語\n- '; diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js index 5030adae2fa..8d2891750e9 100644 --- a/spec/frontend/pages/import/history/components/import_history_app_spec.js +++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js @@ -61,6 +61,7 @@ describe('ImportHistoryApp', () => { const originalApiVersion = gon.api_version; beforeAll(() => { gon.api_version = 'v4'; + gon.features = { fullPathProjectSearch: true }; }); afterAll(() => { diff --git a/spec/frontend/tracking/get_standard_context_spec.js b/spec/frontend/tracking/get_standard_context_spec.js index ada914b586c..ae452aeaeb3 100644 --- a/spec/frontend/tracking/get_standard_context_spec.js +++ b/spec/frontend/tracking/get_standard_context_spec.js @@ -52,7 +52,7 @@ describe('~/tracking/get_standard_context', () => { it('accepts optional `extra` property', () => { const extra = { foo: 'bar' }; - expect(getStandardContext({ extra }).data.extra).toBe(extra); + expect(getStandardContext({ extra }).data.extra).toStrictEqual(extra); }); describe('with Google Analytics cookie present', () => { diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js index 94e1ece8c6b..458f2cc5374 100644 --- a/spec/frontend/vue_shared/components/header_ci_component_spec.js +++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js @@ -1,7 +1,7 @@ import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue'; +import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; import HeaderCi from '~/vue_shared/components/header_ci_component.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -28,7 +28,7 @@ describe('Header CI Component', () => { hasSidebarButton: true, }; - const findIconBadge = () => wrapper.findComponent(CiIconBadge); + const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink); const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip); const findUserLink = () => wrapper.findComponent(GlAvatarLink); const findSidebarToggleBtn = () => wrapper.findComponent(GlButton); @@ -59,7 +59,7 @@ describe('Header CI Component', () => { }); it('should render status badge', () => { - expect(findIconBadge().exists()).toBe(true); + expect(findCiBadgeLink().exists()).toBe(true); }); it('should render timeago date', () => { diff --git a/spec/frontend/webhooks/components/test_dropdown_spec.js b/spec/frontend/webhooks/components/test_dropdown_spec.js new file mode 100644 index 00000000000..2f62ca13469 --- /dev/null +++ b/spec/frontend/webhooks/components/test_dropdown_spec.js @@ -0,0 +1,63 @@ +import { GlDisclosureDropdown } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { getByRole } from '@testing-library/dom'; +import HookTestDropdown from '~/webhooks/components/test_dropdown.vue'; + +const mockItems = [ + { + text: 'Foo', + href: '#foo', + }, +]; + +describe('HookTestDropdown', () => { + let wrapper; + + const findDisclosure = () => wrapper.findComponent(GlDisclosureDropdown); + const clickItem = (itemText) => { + const item = getByRole(wrapper.element, 'button', { name: itemText }); + item.dispatchEvent(new MouseEvent('click')); + }; + + const createComponent = (props) => { + wrapper = mount(HookTestDropdown, { + propsData: { + items: mockItems, + ...props, + }, + }); + }; + + it('passes the expected props to GlDisclosureDropdown', () => { + const size = 'small'; + createComponent({ size }); + + expect(findDisclosure().props()).toMatchObject({ + items: mockItems.map((item) => ({ + text: item.text, + })), + size, + }); + }); + + describe('clicking on an item', () => { + beforeEach(() => { + createComponent(); + }); + + it('triggers @rails/ujs data-method=post handling', () => { + const railsEventPromise = new Promise((resolve) => { + document.addEventListener('click', ({ target }) => { + expect(target.tagName).toBe('A'); + expect(target.dataset.method).toBe('post'); + expect(target.getAttribute('href')).toBe(mockItems[0].href); + resolve(); + }); + }); + + clickItem(mockItems[0].text); + + return railsEventPromise; + }); + }); +}); diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb index 98a1f77b414..a6cfbfe86ca 100644 --- a/spec/helpers/hooks_helper_spec.rb +++ b/spec/helpers/hooks_helper_spec.rb @@ -32,17 +32,34 @@ RSpec.describe HooksHelper do end end - describe '#link_to_test_hook' do + describe '#webhook_test_items' do + let(:triggers) { [:push_events, :note_events] } + + it 'returns test items for disclosure' do + expect(helper.webhook_test_items(project_hook, triggers)).to eq([ + { + href: test_hook_path(project_hook, triggers[0]), + text: 'Push events' + }, + { + href: test_hook_path(project_hook, triggers[1]), + text: 'Comments' + } + ]) + end + end + + describe '#test_hook_path' do let(:trigger) { 'push_events' } it 'returns project namespaced link' do - expect(helper.link_to_test_hook(project_hook, trigger)) - .to include("href=\"#{test_project_hook_path(project, project_hook, trigger: trigger)}\"") + expect(helper.test_hook_path(project_hook, trigger)) + .to eq(test_project_hook_path(project, project_hook, trigger: trigger)) end it 'returns admin namespaced link' do - expect(helper.link_to_test_hook(system_hook, trigger)) - .to include("href=\"#{test_admin_hook_path(system_hook, trigger: trigger)}\"") + expect(helper.test_hook_path(system_hook, trigger)) + .to eq(test_admin_hook_path(system_hook, trigger: trigger)) end end diff --git a/spec/lib/gitlab/ci/interpolation/access_spec.rb b/spec/lib/gitlab/ci/interpolation/access_spec.rb new file mode 100644 index 00000000000..9f6108a328d --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/access_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_authoring do + subject { described_class.new(access, ctx) } + + let(:access) do + 'inputs.data' + end + + let(:ctx) do + { inputs: { data: 'abcd' }, env: { 'ENV' => 'dev' } } + end + + it 'properly evaluates the access pattern' do + expect(subject.value).to eq 'abcd' + end + + context 'when there are too many objects in the access path' do + let(:access) { 'a.b.c.d.e.f.g.h' } + + it 'only support MAX_ACCESS_OBJECTS steps' do + expect(subject.objects.count).to eq 5 + end + end + + context 'when access expression size is too large' do + before do + stub_const("#{described_class}::MAX_ACCESS_BYTESIZE", 10) + end + + it 'returns an error' do + expect(subject).not_to be_valid + expect(subject.errors.first) + .to eq 'maximum interpolation expression size exceeded' + end + end + + context 'when there are not enough objects in the access path' do + let(:access) { 'abc[123]' } + + it 'returns an error when there are no objects found' do + expect(subject).not_to be_valid + expect(subject.errors.first) + .to eq 'invalid interpolation access pattern' + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb new file mode 100644 index 00000000000..7f2be505d17 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/block_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_authoring do + subject { described_class.new(block, data, ctx) } + + let(:data) do + 'inputs.data' + end + + let(:block) do + "$[[ #{data} ]]" + end + + let(:ctx) do + { inputs: { data: 'abc' }, env: { 'ENV' => 'dev' } } + end + + it 'knows its content' do + expect(subject.content).to eq 'inputs.data' + end + + it 'properly evaluates the access pattern' do + expect(subject.value).to eq 'abc' + end + + describe '.match' do + it 'matches each block in a string' do + expect { |b| described_class.match('$[[ access1 ]] $[[ access2 ]]', &b) } + .to yield_successive_args(['$[[ access1 ]]', 'access1'], ['$[[ access2 ]]', 'access2']) + end + + it 'matches an empty block' do + expect { |b| described_class.match('$[[]]', &b) } + .to yield_with_args('$[[]]', '') + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/config_spec.rb b/spec/lib/gitlab/ci/interpolation/config_spec.rb new file mode 100644 index 00000000000..e5987776e00 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/config_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_authoring do + subject { described_class.new(YAML.safe_load(config)) } + + let(:config) do + <<~CFG + test: + spec: + env: $[[ inputs.env ]] + + $[[ inputs.key ]]: + name: $[[ inputs.key ]] + script: my-value + CFG + end + + describe '#replace!' do + it 'replaces each od the nodes with a block return value' do + result = subject.replace! { |node| "abc#{node}cde" } + + expect(result).to eq({ + 'abctestcde' => { 'abcspeccde' => { 'abcenvcde' => 'abc$[[ inputs.env ]]cde' } }, + 'abc$[[ inputs.key ]]cde' => { + 'abcnamecde' => 'abc$[[ inputs.key ]]cde', + 'abcscriptcde' => 'abcmy-valuecde' + } + }) + end + end + + context 'when config size is exceeded' do + before do + stub_const("#{described_class}::MAX_NODES", 7) + end + + it 'returns a config size error' do + replaced = 0 + + subject.replace! { replaced += 1 } + + expect(replaced).to eq 4 + expect(subject.errors.size).to eq 1 + expect(subject.errors.first).to eq 'config too large' + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/context_spec.rb b/spec/lib/gitlab/ci/interpolation/context_spec.rb new file mode 100644 index 00000000000..ada896f4980 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/context_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_authoring do + subject { described_class.new(ctx) } + + let(:ctx) do + { inputs: { key: 'abc' } } + end + + describe '#depth' do + it 'returns a max depth of the hash' do + expect(subject.depth).to eq 2 + end + end + + context 'when interpolation context is too complex' do + let(:ctx) do + { inputs: { key: { aaa: { bbb: 'ccc' } } } } + end + + it 'raises an exception' do + expect { described_class.new(ctx) } + .to raise_error(described_class::ContextTooComplexError) + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/template_spec.rb b/spec/lib/gitlab/ci/interpolation/template_spec.rb new file mode 100644 index 00000000000..8a243b4db05 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/template_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_authoring do + subject { described_class.new(YAML.safe_load(config), ctx) } + + let(:config) do + <<~CFG + test: + spec: + env: $[[ inputs.env ]] + + $[[ inputs.key ]]: + name: $[[ inputs.key ]] + script: my-value + CFG + end + + let(:ctx) do + { inputs: { env: 'dev', key: 'abc' } } + end + + it 'collects interpolation blocks' do + expect(subject.size).to eq 2 + end + + it 'interpolates the values properly' do + expect(subject.interpolated).to eq YAML.safe_load <<~RESULT + test: + spec: + env: dev + + abc: + name: abc + script: my-value + RESULT + end + + context 'when interpolation can not be performed' do + let(:config) { '$[[ xxx.yyy ]]: abc' } + + it 'does not interpolate the config' do + expect(subject).not_to be_valid + expect(subject.interpolated).to be_nil + end + end + + context 'when template consists of nested arrays with hashes and values' do + let(:config) do + <<~CFG + test: + - a-$[[ inputs.key ]]-b + - c-$[[ inputs.key ]]-d: + d-$[[ inputs.key ]]-e + val: 1 + CFG + end + + it 'performs a valid interpolation' do + result = { 'test' => ['a-abc-b', { 'c-abc-d' => 'd-abc-e', 'val' => 1 }] } + + expect(subject).to be_valid + expect(subject.interpolated).to eq result + end + end + + context 'when template contains symbols that need interpolation' do + subject do + described_class.new({ '$[[ inputs.key ]]'.to_sym => 'cde' }, ctx) + end + + it 'performs a valid interpolation' do + expect(subject).to be_valid + expect(subject.interpolated).to eq({ 'abc' => 'cde' }) + end + end + + context 'when template is too large' do + before do + stub_const('Gitlab::Ci::Interpolation::Config::MAX_NODES', 1) + end + + it 'returns an error' do + expect(subject.interpolated).to be_nil + expect(subject.errors.count).to eq 1 + expect(subject.errors.first).to eq 'config too large' + end + end + + context 'when there are too many interpolation blocks' do + before do + stub_const("#{described_class}::MAX_BLOCKS", 1) + end + + it 'returns an error' do + expect(subject.interpolated).to be_nil + expect(subject.errors.count).to eq 1 + expect(subject.errors.first).to eq 'too many interpolation blocks' + end + end +end diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb index 7bd3c5743a6..1c9798c6d99 100644 --- a/spec/models/discussion_spec.rb +++ b/spec/models/discussion_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Discussion do +RSpec.describe Discussion, feature_category: :team_planning do subject { described_class.new([first_note, second_note, third_note]) } let(:first_note) { create(:diff_note_on_merge_request) } @@ -70,4 +70,67 @@ RSpec.describe Discussion do end end end + + describe '#to_global_id' do + context 'with a single DiffNote discussion' do + it 'returns GID on Discussion class' do + discussion = described_class.build([first_note], merge_request) + discussion_id = discussion.id + + expect(discussion.class.name.to_s).to eq("DiffDiscussion") + expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}") + end + end + + context 'with multiple DiffNotes discussion' do + it 'returns GID on Discussion class' do + discussion = described_class.build([first_note, second_note], merge_request) + discussion_id = discussion.id + + expect(discussion.class.name.to_s).to eq("DiffDiscussion") + expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}") + end + end + + context 'with discussions on issue' do + let_it_be(:note_1, refind: true) { create(:note) } + let_it_be(:noteable) { note_1.noteable } + + context 'with a single Note' do + it 'returns GID on Discussion class' do + discussion = described_class.build([note_1], noteable) + discussion_id = discussion.id + + expect(discussion.class.name.to_s).to eq("IndividualNoteDiscussion") + expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}") + end + end + + context 'with multiple Notes' do + let_it_be(:note_1, refind: true) { create(:note, type: 'DiscussionNote') } + let_it_be(:note_2, refind: true) { create(:note, in_reply_to: note_1) } + + it 'returns GID on Discussion class' do + discussion = described_class.build([note_1, note_2], noteable) + discussion_id = discussion.id + + expect(discussion.class.name.to_s).to eq("Discussion") + expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}") + end + end + end + + context 'with system notes' do + let_it_be(:system_note, refind: true) { create(:note, system: true) } + let_it_be(:noteable) { system_note.noteable } + + it 'returns GID on Discussion class' do + discussion = described_class.build([system_note], noteable) + discussion_id = discussion.id + + expect(discussion.class.name.to_s).to eq("IndividualNoteDiscussion") + expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}") + end + end + end end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb index 3907ebad9ce..1898ee5a62d 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb @@ -63,4 +63,20 @@ RSpec.describe 'Setting milestone of a merge request', feature_category: :code_r expect(mutation_response['mergeRequest']['milestone']).to be_nil end end + + context 'when passing an invalid milestone_id' do + let(:input) { { milestone_id: GitlabSchema.id_from_object(create(:milestone)).to_s } } + + it 'does not set the milestone' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to include( + a_hash_including( + 'message' => "The resource that you are attempting to access does not exist " \ + "or you don't have permission to perform this action" + ) + ) + end + end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 930766c520b..7fd09cc2779 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -106,29 +106,29 @@ RSpec.describe Issues::UpdateService, :mailer do context 'when updating milestone' do before do - update_issue({ milestone: nil }) + update_issue({ milestone_id: nil }) end it 'updates issue milestone when passing `milestone` param' do - expect { update_issue({ milestone: milestone }) } + expect { update_issue({ milestone_id: milestone.id }) } .to change(issue, :milestone).to(milestone).from(nil) end it "triggers 'issuableMilestoneUpdated'" do expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(issue).and_call_original - update_issue({ milestone: milestone }) + update_issue({ milestone_id: milestone.id }) end context 'when milestone remains unchanged' do before do - update_issue({ title: 'abc', milestone: milestone }) + update_issue({ title: 'abc', milestone_id: milestone.id }) end it "does not trigger 'issuableMilestoneUpdated'" do expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated) - update_issue({ milestone: milestone }) + update_issue({ milestone_id: milestone.id }) end end end @@ -755,14 +755,14 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'marks todos as done' do - update_issue(milestone: create(:milestone, project: project)) + update_issue(milestone_id: create(:milestone, project: project).id) expect(todo.reload.done?).to eq true end it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do perform_enqueued_jobs do - update_issue(milestone: create(:milestone, project: project)) + update_issue(milestone_id: create(:milestone, project: project).id) end should_email(subscriber) @@ -779,7 +779,7 @@ RSpec.describe Issues::UpdateService, :mailer do expect(service).to receive(:delete_cache).and_call_original end - update_issue(milestone: milestone) + update_issue(milestone_id: milestone.id) end end @@ -803,7 +803,7 @@ RSpec.describe Issues::UpdateService, :mailer do expect(service).to receive(:delete_cache).and_call_original end - update_issue(milestone: new_milestone) + update_issue(milestone_id: new_milestone.id) end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 344d93fc5ca..e20ebf18e7c 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -196,7 +196,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) .to receive(:track_milestone_changed_action).once.with(user: user) - opts[:milestone] = milestone + opts[:milestone_id] = milestone.id MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end @@ -236,27 +236,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re end context 'updating milestone' do - RSpec.shared_examples 'updates milestone' do + context 'with milestone_id param' do + let(:opts) { { milestone_id: milestone.id } } + it 'sets milestone' do expect(@merge_request.milestone).to eq milestone end end - context 'when milestone_id param' do - let(:opts) { { milestone_id: milestone.id } } - - it_behaves_like 'updates milestone' - end - - context 'when milestone param' do - let(:opts) { { milestone: milestone } } - - it_behaves_like 'updates milestone' - end - context 'milestone counters cache reset' do let(:milestone_old) { create(:milestone, project: project) } - let(:opts) { { milestone: milestone_old } } + let(:opts) { { milestone_id: milestone_old.id } } it 'deletes milestone counters' do expect_next_instance_of(Milestones::MergeRequestsCountService, milestone_old) do |service| @@ -267,7 +257,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(service).to receive(:delete_cache).and_call_original end - update_merge_request(milestone: milestone) + update_merge_request(milestone_id: milestone.id) end it 'deletes milestone counters when the milestone is removed' do @@ -275,17 +265,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(service).to receive(:delete_cache).and_call_original end - update_merge_request(milestone: nil) + update_merge_request(milestone_id: nil) end it 'deletes milestone counters when the milestone was not set' do - update_merge_request(milestone: nil) + update_merge_request(milestone_id: nil) expect_next_instance_of(Milestones::MergeRequestsCountService, milestone) do |service| expect(service).to receive(:delete_cache).and_call_original end - update_merge_request(milestone: milestone) + update_merge_request(milestone_id: milestone.id) end end end @@ -754,12 +744,12 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(service).to receive(:async_execute) end - update_merge_request({ milestone: create(:milestone, project: project) }) + update_merge_request(milestone_id: create(:milestone, project: project).id) end it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do perform_enqueued_jobs do - update_merge_request(milestone: create(:milestone, project: project)) + update_merge_request(milestone_id: create(:milestone, project: project).id) end should_email(subscriber) diff --git a/workhorse/go.mod b/workhorse/go.mod index 7c905f6e2b3..582903f9721 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -26,7 +26,7 @@ require ( github.com/sirupsen/logrus v1.9.0 github.com/smartystreets/goconvey v1.7.2 github.com/stretchr/testify v1.8.1 - gitlab.com/gitlab-org/gitaly/v15 v15.7.5 + gitlab.com/gitlab-org/gitaly/v15 v15.8.0 gitlab.com/gitlab-org/golang-archive-zip v0.1.1 gitlab.com/gitlab-org/labkit v1.17.0 gocloud.dev v0.28.0 @@ -107,13 +107,13 @@ require ( github.com/uber/jaeger-lib v2.4.1+incompatible // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.10.0 // indirect - golang.org/x/crypto v0.3.0 // indirect + golang.org/x/crypto v0.5.0 // indirect golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect golang.org/x/mod v0.6.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect - golang.org/x/time v0.2.0 // indirect + golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.103.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/workhorse/go.sum b/workhorse/go.sum index b1cb3634898..f8b510b88fa 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -1842,8 +1842,8 @@ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/gitlab-org/gitaly/v15 v15.7.5 h1:qdPq0AfRDZuf7lelYorWWGOUAnxbz67XGQ5VPXhnsmM= -gitlab.com/gitlab-org/gitaly/v15 v15.7.5/go.mod h1:s37u+W94lg3T7cv+i+v5WtstyHvuKV1JlwYJNznZVJE= +gitlab.com/gitlab-org/gitaly/v15 v15.8.0 h1:v72gyGuzM2XTLWodIfOBDEzCj9bRt8wiQVT2RIQ37F8= +gitlab.com/gitlab-org/gitaly/v15 v15.8.0/go.mod h1:typ7BQ9JuglkeTvW3Vn1SwFcZ1FMn8P3GrWcPuZQ0EU= gitlab.com/gitlab-org/golang-archive-zip v0.1.1 h1:35k9giivbxwF03+8A05Cm8YoxoakU8FBCj5gysjCTCE= gitlab.com/gitlab-org/golang-archive-zip v0.1.1/go.mod h1:ZDtqpWPGPB9qBuZnZDrKQjIdJtkN7ZAoVwhT6H2o2kE= gitlab.com/gitlab-org/labkit v1.17.0 h1:mEkoLzXorLNdt8NkfgYS5xMDhdqCsIJaeEVtSf7d8cU= @@ -1987,8 +1987,9 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -2360,8 +2361,9 @@ golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/yarn.lock b/yarn.lock index ca31d3a0b27..7eb3bc2e9f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2664,11 +2664,6 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -JSV@^4.0.x: - version "4.0.2" - resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" - integrity sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw== - abab@^2.0.5, abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -2837,11 +2832,6 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" - integrity sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA== - anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -3555,15 +3545,6 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" - integrity sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ== - dependencies: - ansi-styles "~1.0.0" - has-color "~0.1.0" - strip-ansi "~0.1.0" - char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -6539,11 +6520,6 @@ has-bigints@^1.0.1, has-bigints@^1.0.2: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== -has-color@~0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" - integrity sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw== - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -7986,14 +7962,6 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" -jsonlint@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.3.tgz#cb5e31efc0b78291d0d862fbef05900adf212988" - integrity sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A== - dependencies: - JSV "^4.0.x" - nomnom "^1.5.x" - jszip@^3.1.3: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -9427,14 +9395,6 @@ nodemon@^2.0.19: touch "^3.1.0" undefsafe "^2.0.5" -nomnom@^1.5.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" - integrity sha512-5s0JxqhDx9/rksG2BTMVN1enjWSvPidpoSgViZU4ZXULyTe+7jxcCRLB6f42Z0l1xYJpleCBtSyY6Lwg3uu5CQ== - dependencies: - chalk "~0.4.0" - underscore "~1.6.0" - nopt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" @@ -11443,11 +11403,6 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" - integrity sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg== - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -12058,11 +12013,6 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== -underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - integrity sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ== - undici@^5.0.0: version "5.8.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.8.0.tgz#dec9a8ccd90e5a1d81d43c0eab6503146d649a4f" |