diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-16 09:12:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-16 09:12:24 +0300 |
commit | 885897989971931fcb139969b49d8b06f907d7d0 (patch) | |
tree | 7c2873a6690b73d0036b56bf379634c235dfb438 | |
parent | f5c9eb81b000010cf8abc91cbcfd1d6537f66c64 (diff) |
Add latest changes from gitlab-org/gitlab@master
61 files changed, 849 insertions, 99 deletions
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js index 4ae76050aa5..b856ac6c627 100644 --- a/app/assets/javascripts/design_management/index.js +++ b/app/assets/javascripts/design_management/index.js @@ -24,6 +24,7 @@ export default () => { return new Vue({ el, + name: 'DesignRoot', router, apolloProvider, provide: { diff --git a/app/assets/javascripts/emoji/awards_app/index.js b/app/assets/javascripts/emoji/awards_app/index.js index 1a084d37762..0986533dcd1 100644 --- a/app/assets/javascripts/emoji/awards_app/index.js +++ b/app/assets/javascripts/emoji/awards_app/index.js @@ -12,6 +12,7 @@ export default (el) => { return new Vue({ el, + name: 'AwardsListRoot', store: createstore(), computed: { ...mapState(['currentUserId', 'canAwardEmoji', 'awards']), diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 588b1c9ef52..e9d620cedf0 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -28,6 +28,7 @@ export default function initInviteMembersModal() { return new Vue({ el, + name: 'InviteMembersModalRoot', provide: { newProjectPath: el.dataset.newProjectPath, }, diff --git a/app/assets/javascripts/invite_members/init_invite_members_trigger.js b/app/assets/javascripts/invite_members/init_invite_members_trigger.js index 935edb35349..54a5eab2e4b 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_trigger.js +++ b/app/assets/javascripts/invite_members/init_invite_members_trigger.js @@ -11,6 +11,7 @@ export default function initInviteMembersTrigger() { return triggers.forEach((el) => { return new Vue({ el, + name: 'InviteMembersTriggerRoot', render: (createElement) => createElement(InviteMembersTrigger, { props: { diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js index 57bad5182e7..00b027523e2 100644 --- a/app/assets/javascripts/issuable/index.js +++ b/app/assets/javascripts/issuable/index.js @@ -97,6 +97,7 @@ export function initIssuableHeaderWarnings(store) { return new Vue({ el, + name: 'IssuableHeaderWarningsRoot', store, provide: { hidden: parseBoolean(hidden) }, render: (createElement) => createElement(IssuableHeaderWarnings), diff --git a/app/assets/javascripts/issues/related_merge_requests/index.js b/app/assets/javascripts/issues/related_merge_requests/index.js index 5045f7e1a2a..196084093c8 100644 --- a/app/assets/javascripts/issues/related_merge_requests/index.js +++ b/app/assets/javascripts/issues/related_merge_requests/index.js @@ -13,6 +13,7 @@ export function initRelatedMergeRequests() { return new Vue({ el, + name: 'RelatedMergeRequestsRoot', store: createStore(), render: (createElement) => createElement(RelatedMergeRequests, { diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js index bf4debcdfa9..732bdc09aea 100644 --- a/app/assets/javascripts/issues/show/index.js +++ b/app/assets/javascripts/issues/show/index.js @@ -44,6 +44,7 @@ export function initIncidentApp(issueData = {}) { return new Vue({ el, + name: 'DescriptionRoot', apolloProvider, provide: { issueType: INCIDENT_TYPE, @@ -86,6 +87,7 @@ export function initIssueApp(issueData, store) { return new Vue({ el, + name: 'DescriptionRoot', apolloProvider, store, provide: { @@ -123,6 +125,7 @@ export function initHeaderActions(store, type = '') { return new Vue({ el, + name: 'HeaderActionsRoot', apolloProvider, store, provide: { diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue index 733d0f69f5d..f3380b7b4ba 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -1,13 +1,21 @@ <script> -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui'; import { __ } from '~/locale'; export default { cancelAction: { text: __('Cancel') }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, components: { GlModal, }, props: { + title: { + type: String, + required: false, + default: '', + }, primaryText: { type: String, required: false, @@ -18,11 +26,27 @@ export default { required: false, default: 'confirm', }, + modalHtmlMessage: { + type: String, + required: false, + default: '', + }, + hideCancel: { + type: Boolean, + required: false, + default: false, + }, }, computed: { primaryAction() { return { text: this.primaryText, attributes: { variant: this.primaryVariant } }; }, + cancelAction() { + return this.hideCancel ? null : this.$options.cancelAction; + }, + shouldShowHeader() { + return Boolean(this.title?.length); + }, }, mounted() { this.$refs.modal.show(); @@ -36,12 +60,14 @@ export default { size="sm" modal-id="confirmationModal" body-class="gl-display-flex" + :title="title" :action-primary="primaryAction" - :action-cancel="$options.cancelAction" - hide-header + :action-cancel="cancelAction" + :hide-header="!shouldShowHeader" @primary="$emit('confirmed')" @hidden="$emit('closed')" > - <div class="gl-align-self-center"><slot></slot></div> + <div v-if="!modalHtmlMessage" class="gl-align-self-center"><slot></slot></div> + <div v-else v-safe-html="modalHtmlMessage" class="gl-align-self-center"></div> </gl-modal> </template> diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js index fdd0e045d07..a8a89d0644a 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js @@ -1,6 +1,9 @@ import Vue from 'vue'; -export function confirmAction(message, { primaryBtnVariant, primaryBtnText } = {}) { +export function confirmAction( + message, + { primaryBtnVariant, primaryBtnText, modalHtmlMessage, title, hideCancel } = {}, +) { return new Promise((resolve) => { let confirmed = false; @@ -15,6 +18,9 @@ export function confirmAction(message, { primaryBtnVariant, primaryBtnText } = { props: { primaryVariant: primaryBtnVariant, primaryText: primaryBtnText, + title, + modalHtmlMessage, + hideCancel, }, on: { confirmed() { diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 40dd29bea76..ec6789d81ec 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -5,6 +5,12 @@ import { insertText } from '~/lib/utils/common_utils'; const LINK_TAG_PATTERN = '[{text}](url)'; +// at the start of a line, find any amount of whitespace followed by +// a bullet point character (*+-) and an optional checkbox ([ ] [x]) +// OR a number with a . after it and an optional checkbox ([ ] [x]) +// followed by one or more whitespace characters +const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isOl>[*+-])|(?<isUl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/; + function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } @@ -13,8 +19,15 @@ function addBlockTags(blockTag, selected) { return `${blockTag}\n${selected}\n${blockTag}`; } -function lineBefore(text, textarea) { - const split = text.substring(0, textarea.selectionStart).trim().split('\n'); +function lineBefore(text, textarea, trimNewlines = true) { + let split = text.substring(0, textarea.selectionStart); + + if (trimNewlines) { + split = split.trim(); + } + + split = split.split('\n'); + return split[split.length - 1]; } @@ -284,9 +297,9 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo } /* eslint-disable @gitlab/require-i18n-strings */ -export function keypressNoteText(e) { +function handleSurroundSelectedText(e, textArea) { if (!gon.markdown_surround_selection) return; - if (this.selectionStart === this.selectionEnd) return; + if (textArea.selectionStart === textArea.selectionEnd) return; const keys = { '*': '**{text}**', // wraps with bold character @@ -306,7 +319,7 @@ export function keypressNoteText(e) { updateText({ tag, - textArea: this, + textArea, blockTag: '', wrap: true, select: '', @@ -316,6 +329,48 @@ export function keypressNoteText(e) { } /* eslint-enable @gitlab/require-i18n-strings */ +function handleContinueList(e, textArea) { + if (!gon.features?.markdownContinueLists) return; + if (!(e.key === 'Enter')) return; + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + if (textArea.selectionStart !== textArea.selectionEnd) return; + + const currentLine = lineBefore(textArea.value, textArea, false); + const result = currentLine.match(LIST_LINE_HEAD_PATTERN); + + if (result) { + const { indent, content, leader } = result.groups; + const prevLineEmpty = !content; + + if (prevLineEmpty) { + // erase previous empty list item - select the text and allow the + // natural line feed erase the text + textArea.selectionStart = textArea.selectionStart - result[0].length; + return; + } + + const itemInsert = `${indent}${leader}`; + + e.preventDefault(); + + updateText({ + tag: itemInsert, + textArea, + blockTag: '', + wrap: false, + select: '', + tagContent: '', + }); + } +} + +export function keypressNoteText(e) { + const textArea = this; + + handleContinueList(e, textArea); + handleSurroundSelectedText(e, textArea); +} + export function updateTextForToolbarBtn($toolbarBtn) { return updateText({ textArea: $toolbarBtn.closest('.md-area').find('textarea'), diff --git a/app/assets/javascripts/nav/mount.js b/app/assets/javascripts/nav/mount.js index 51b6a31b8cb..7b0cc977107 100644 --- a/app/assets/javascripts/nav/mount.js +++ b/app/assets/javascripts/nav/mount.js @@ -12,6 +12,7 @@ const mount = (el, Component) => { return new Vue({ el, + name: 'TopNavRoot', store, render(h) { return h(Component, { diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index 7c9e7703d59..104e9d4183a 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -19,7 +19,7 @@ export default (store) => { return new Vue({ el: discussionFilterEl, - name: 'DiscussionFilter', + name: 'DiscussionFilterRoot', components: { DiscussionFilter, }, diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 2ce60976adb..19fa484d659 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -14,6 +14,7 @@ export default () => { // eslint-disable-next-line no-new new Vue({ el, + name: 'NotesRoot', components: { notesApp, }, diff --git a/app/assets/javascripts/notes/sort_discussions.js b/app/assets/javascripts/notes/sort_discussions.js index ecfa3223039..ca8df880fe4 100644 --- a/app/assets/javascripts/notes/sort_discussions.js +++ b/app/assets/javascripts/notes/sort_discussions.js @@ -8,6 +8,7 @@ export default (store) => { return new Vue({ el, + name: 'SortDiscussionRoot', store, render(createElement) { return createElement(SortDiscussion); diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index 66e999ca43b..eb5b50dd1ec 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -20,6 +20,7 @@ const initPerformanceBar = (el) => { return new Vue({ el, + name: 'PerformanceBarRoot', components: { PerformanceBarApp: () => import('./components/performance_bar_app.vue'), }, diff --git a/app/assets/javascripts/popovers/index.js b/app/assets/javascripts/popovers/index.js index 7db669b8c52..94340ae16a0 100644 --- a/app/assets/javascripts/popovers/index.js +++ b/app/assets/javascripts/popovers/index.js @@ -13,7 +13,7 @@ const getPopoversApp = () => { document.body.appendChild(container); const Popovers = Vue.extend(PopoversComponent); - app = new Popovers(); + app = new Popovers({ name: 'PopoversRoot' }); app.$mount(`#${APP_ELEMENT_ID}`); } diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 55913394ca3..62e2cec874a 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import { debounce } from 'lodash'; import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '../lib/utils/constants'; import axios from '../lib/utils/axios_utils'; import { @@ -105,6 +106,21 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => { }; const bindHowToImport = () => { + const importLinks = document.querySelectorAll('.js-how-to-import-link'); + + importLinks.forEach((link) => { + const { modalTitle: title, modalMessage: modalHtmlMessage } = link.dataset; + + link.addEventListener('click', (e) => { + e.preventDefault(); + confirmAction('', { + modalHtmlMessage, + title, + hideCancel: true, + }); + }); + }); + $('.how_to_import_link').on('click', (e) => { e.preventDefault(); $(e.currentTarget).next('.modal').show(); diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js index bd9845e616b..35858be90b2 100644 --- a/app/assets/javascripts/related_issues/index.js +++ b/app/assets/javascripts/related_issues/index.js @@ -8,7 +8,7 @@ export default function initRelatedIssues() { // eslint-disable-next-line no-new new Vue({ el: relatedIssuesRootElement, - name: 'RelatedIssuesApp', + name: 'RelatedIssuesRoot', components: { relatedIssuesRoot: RelatedIssuesRoot, }, diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 6363422259e..c29784aa328 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -57,6 +57,7 @@ function mountSidebarToDoWidget() { return new Vue({ el, + name: 'SidebarTodoRoot', apolloProvider, components: { SidebarTodoWidget, @@ -103,6 +104,7 @@ function mountAssigneesComponentDeprecated(mediator) { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarAssigneesRoot', apolloProvider, components: { SidebarAssignees, @@ -135,6 +137,7 @@ function mountAssigneesComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarAssigneesRoot', apolloProvider, components: { SidebarAssigneesWidget, @@ -185,6 +188,7 @@ function mountReviewersComponent(mediator) { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarReviewersRoot', apolloProvider, components: { SidebarReviewers, @@ -218,6 +222,7 @@ function mountCrmContactsComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarCrmContactsRoot', apolloProvider, components: { CrmContacts, @@ -242,6 +247,7 @@ function mountMilestoneSelect() { return new Vue({ el, + name: 'SidebarMilestoneRoot', apolloProvider, components: { SidebarDropdownWidget, @@ -274,6 +280,7 @@ export function mountSidebarLabels() { return new Vue({ el, + name: 'SidebarLabelsRoot', apolloProvider, components: { @@ -328,6 +335,7 @@ function mountConfidentialComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarConfidentialRoot', apolloProvider, components: { SidebarConfidentialityWidget, @@ -362,6 +370,7 @@ function mountDueDateComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarDueDateRoot', apolloProvider, components: { SidebarDueDateWidget, @@ -392,6 +401,7 @@ function mountReferenceComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarReferenceRoot', apolloProvider, components: { SidebarReferenceWidget, @@ -428,6 +438,7 @@ function mountLockComponent(store) { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarLockRoot', store, provide: { fullPath, @@ -451,6 +462,7 @@ function mountParticipantsComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarParticipantsRoot', apolloProvider, components: { SidebarParticipantsWidget, @@ -479,6 +491,7 @@ function mountSubscriptionsComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarSubscriptionsRoot', apolloProvider, components: { SidebarSubscriptionsWidget, @@ -509,6 +522,7 @@ function mountTimeTrackingComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarTimeTrackingRoot', apolloProvider, provide: { issuableType }, render: (createElement) => @@ -534,6 +548,7 @@ function mountSeverityComponent() { return new Vue({ el: severityContainerEl, + name: 'SidebarSeverityRoot', apolloProvider, components: { SidebarSeverity, @@ -562,6 +577,7 @@ function mountCopyEmailComponent() { // eslint-disable-next-line no-new new Vue({ el, + name: 'SidebarCopyEmailRoot', render: (createElement) => createElement(CopyEmailToClipboard, { props: { issueEmailAddress: createNoteEmail } }), }); diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js index 49a43b120e0..4639671984a 100644 --- a/app/assets/javascripts/tooltips/index.js +++ b/app/assets/javascripts/tooltips/index.js @@ -21,6 +21,7 @@ const tooltipsApp = () => { document.body.appendChild(container); app = new Vue({ + name: 'TooltipsRoot', render(h) { return h(Tooltips, { props: { diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index d6aa63be996..57c9bcde024 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -200,6 +200,12 @@ } } +.gl-xl-ml-3 { + @include media-breakpoint-up(lg) { + margin-left: $gl-spacing-scale-3; + } +} + .gl-mb-n3 { margin-bottom: -$gl-spacing-scale-3; } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 281e21b3ab0..1b98810b09b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -46,14 +46,15 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:vue_issues_list, project&.group, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml) push_frontend_feature_flag(:contacts_autocomplete, project&.group, default_enabled: :yaml) + push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml) end before_action only: :show do - push_frontend_feature_flag(:real_time_issue_sidebar, @project, default_enabled: :yaml) + push_frontend_feature_flag(:real_time_issue_sidebar, project, default_enabled: :yaml) push_frontend_feature_flag(:confidential_notes, project&.group, default_enabled: :yaml) - push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml) - push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml) - push_frontend_feature_flag(:fix_comment_scroll, @project, default_enabled: :yaml) + push_frontend_feature_flag(:issue_assignees_widget, project, default_enabled: :yaml) + push_frontend_feature_flag(:paginated_issue_discussions, project, default_enabled: :yaml) + push_frontend_feature_flag(:fix_comment_scroll, project, default_enabled: :yaml) push_frontend_feature_flag(:work_items, project, default_enabled: :yaml) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 05452ef6578..6445f920db5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -36,20 +36,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action only: [:show] do push_frontend_feature_flag(:file_identifier_hash) - push_frontend_feature_flag(:merge_request_widget_graphql, @project, default_enabled: :yaml) - push_frontend_feature_flag(:default_merge_ref_for_diffs, @project, default_enabled: :yaml) - push_frontend_feature_flag(:core_security_mr_widget_counts, @project) - push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml) - push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml) + push_frontend_feature_flag(:merge_request_widget_graphql, project, default_enabled: :yaml) + push_frontend_feature_flag(:default_merge_ref_for_diffs, project, default_enabled: :yaml) + push_frontend_feature_flag(:core_security_mr_widget_counts, project) + push_frontend_feature_flag(:paginated_notes, project, default_enabled: :yaml) + push_frontend_feature_flag(:confidential_notes, project, default_enabled: :yaml) push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml) push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml) - push_frontend_feature_flag(:refactor_mr_widgets_extensions, @project, default_enabled: :yaml) - push_frontend_feature_flag(:rebase_without_ci_ui, @project, default_enabled: :yaml) + push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml) + push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml) push_frontend_feature_flag(:rearrange_pipelines_table, project, default_enabled: :yaml) + push_frontend_feature_flag(:markdown_continue_lists, project, default_enabled: :yaml) # Usage data feature flags - push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml) + push_frontend_feature_flag(:users_expanding_widgets_usage_data, project, default_enabled: :yaml) push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml) - push_frontend_feature_flag(:usage_data_diff_searches, @project, default_enabled: :yaml) + push_frontend_feature_flag(:usage_data_diff_searches, project, default_enabled: :yaml) end before_action do diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index d58ed252a36..e38eeaed367 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -144,6 +144,7 @@ class SearchController < ApplicationController payload[:metadata]['meta.search.filters.state'] = params[:state] payload[:metadata]['meta.search.force_search_results'] = params[:force_search_results] payload[:metadata]['meta.search.project_ids'] = params[:project_ids] + payload[:metadata]['meta.search.search_level'] = params[:search_level] if search_service.abuse_detected? payload[:metadata]['abuse.confidence'] = Gitlab::Abuse.confidence(:certain) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b39cd485fe8..6098ef63ec3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -430,6 +430,18 @@ module ProjectsHelper end end + def import_from_bitbucket_message + link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/bitbucket") } + + str = if current_user.admin? + 'ImportProjects|To enable importing projects from Bitbucket, as administrator you need to configure %{link_start}OAuth integration%{link_end}' + else + 'ImportProjects|To enable importing projects from Bitbucket, ask your GitLab administrator to configure %{link_start}OAuth integration%{link_end}' + end + + s_(str).html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + end + private def tab_ability_map diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 3320c13e87b..7e538238cbd 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -50,8 +50,9 @@ class WebHook < ApplicationRecord end # rubocop: disable CodeReuse/ServiceClass - def execute(data, hook_name) - WebHookService.new(self, data, hook_name).execute if executable? + def execute(data, hook_name, force: false) + # hook.executable? is checked in WebHookService#execute + WebHookService.new(self, data, hook_name, force: force).execute end # rubocop: enable CodeReuse/ServiceClass diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index 0fda6fb1ed0..b41a9959c13 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -18,7 +18,7 @@ module TestHooks return error('Testing not available for this hook') if trigger_key.nil? || data.blank? return error(data[:error]) if data[:error].present? - hook.execute(data, trigger_key) + hook.execute(data, trigger_key, force: true) end end end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index a4342aa5b90..b1d8872aa5e 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -34,11 +34,12 @@ class WebHookService hook_name.to_s.singularize.titleize end - def initialize(hook, data, hook_name, uniqueness_token = nil) + def initialize(hook, data, hook_name, uniqueness_token = nil, force: false) @hook = hook @data = data @hook_name = hook_name.to_s @uniqueness_token = uniqueness_token + @force = force @request_options = { timeout: Gitlab.config.gitlab.webhook_timeout, use_read_total_timeout: true, @@ -46,8 +47,12 @@ class WebHookService } end + def disabled? + !@force && !hook.executable? + end + def execute - return { status: :error, message: 'Hook disabled' } unless hook.executable? + return { status: :error, message: 'Hook disabled' } if disabled? if recursion_blocked? log_recursion_blocked @@ -148,8 +153,12 @@ class WebHookService internal_error_message: error_message } - ::WebHooks::LogExecutionWorker - .perform_async(hook.id, log_data, category, uniqueness_token) + if @force # executed as part of test - run log-execution inline. + ::WebHooks::LogExecutionService.new(hook: hook, log_data: log_data, response_category: category).execute + else + ::WebHooks::LogExecutionWorker + .perform_async(hook.id, log_data, category, uniqueness_token) + end end def response_category(response) diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml deleted file mode 100644 index 1379a339feb..00000000000 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -#bitbucket_import_modal.modal - .modal-dialog - .modal-content - .modal-header - %h3.modal-title Import projects from Bitbucket - %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } - %span{ "aria-hidden": "true" } × - .modal-body - To enable importing projects from Bitbucket, - - if current_user.admin? - as administrator you need to configure - - else - ask your GitLab administrator to configure - = link_to 'OAuth integration', help_page_path("integration/bitbucket") diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index f8f96dd77c4..aca7b73267b 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -22,13 +22,11 @@ - if bitbucket_import_enabled? %div - = link_to status_import_bitbucket_path, class: "gl-button btn-default btn import_bitbucket js-import-project-btn #{'how_to_import_link' unless bitbucket_import_configured?}", - data: { platform: 'bitbucket_cloud', **tracking_attrs_data(track_label, 'click_button', 'bitbucket_cloud') } do + = link_to status_import_bitbucket_path, class: "gl-button btn-default btn import_bitbucket js-import-project-btn #{'js-how-to-import-link' unless bitbucket_import_configured?}", + data: { modal_title: _("Import projects from Bitbucket"), modal_message: import_from_bitbucket_message, platform: 'bitbucket_cloud', **tracking_attrs_data(track_label, 'click_button', 'bitbucket_cloud') } do .gl-button-icon = sprite_icon('bitbucket') Bitbucket Cloud - - unless bitbucket_import_configured? - = render 'projects/bitbucket_import_modal' - if bitbucket_server_import_enabled? %div = link_to status_import_bitbucket_server_path, class: "gl-button btn-default btn import_bitbucket js-import-project-btn", data: { platform: 'bitbucket_server', **tracking_attrs_data(track_label, 'click_button', 'bitbucket_server') } do diff --git a/config/feature_flags/development/block_external_fork_network_mirrors.yml b/config/feature_flags/development/block_external_fork_network_mirrors.yml deleted file mode 100644 index 8c313bc9273..00000000000 --- a/config/feature_flags/development/block_external_fork_network_mirrors.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: block_external_fork_network_mirrors -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60735 -rollout_issue_url: -milestone: '14.0' -type: development -group: group::source code -default_enabled: true diff --git a/config/feature_flags/development/markdown_continue_lists.yml b/config/feature_flags/development/markdown_continue_lists.yml new file mode 100644 index 00000000000..8be9a3008da --- /dev/null +++ b/config/feature_flags/development/markdown_continue_lists.yml @@ -0,0 +1,8 @@ +--- +name: markdown_continue_lists +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79161 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351386 +milestone: '14.8' +type: development +group: group::project management +default_enabled: false diff --git a/data/deprecations/14-8-graphql-ids.yml b/data/deprecations/14-8-graphql-ids.yml new file mode 100644 index 00000000000..dd73f310335 --- /dev/null +++ b/data/deprecations/14-8-graphql-ids.yml @@ -0,0 +1,66 @@ +- name: "GraphQL ID and GlobalID compatibility" + announcement_milestone: "14.8" + announcement_date: "2022-02-22" + removal_milestone: "15.0" + removal_date: "2022-04-22" + breaking_change: true + reporter: alexkalderimis + body: | # Do not modify this line, instead modify the lines below. + We are removing a non-standard extension to our GraphQL processor, which we added for backwards compatibility. This extension modifies the validation of GraphQL queries, allowing the use of the `ID` type for arguments where it would normally be rejected. + Some arguments originally had the type `ID`. These were changed to specific + kinds of `ID`. This change may be a breaking change if you: + + - Use GraphQL. + - Use the `ID` type for any argument in your query signatures. + + Some field arguments still have the `ID` type. These are typically for + IID values, or namespace paths. An example is `Query.project(fullPath: ID!)`. + + For a list of affected and unaffected field arguments, + see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/352832). + + You can test if this change affects you by validating + your queries locally, using schema data fetched from a GitLab server. + You can do this by using the GraphQL explorer tool for the relevant GitLab + instance. For example: `https://gitlab.com/-/graphql-explorer`. + + For example, the following query illustrates the breaking change: + + ```graphql + # a query using the deprecated type of Query.issue(id:) + # WARNING: This will not work after GitLab 15.0 + query($id: ID!) { + deprecated: issue(id: $id) { + title, description + } + } + ``` + + The query above will not work after GitLab 15.0 is released, because the type + of `Query.issue(id:)` is actually `IssueID!`. + + Instead, you should use one of the following two forms: + + ```graphql + # This will continue to work + query($id: IssueID!) { + a: issue(id: $id) { + title, description + } + b: issue(id: "gid://gitlab/Issue/12345") { + title, description + } + } + ``` + + This query works now, and will continue to work after GitLab 15.0. + You should convert any queries in the first form (using `ID` as a named type in the signature) + to one of the other two forms (using the correct appropriate type in the signature, or using + an inline argument expression). +# The following items are not published on the docs page, but may be used in the future. + stage: # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth + tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate] + issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/257883' + documentation_url: # (optional) This is a link to the current documentation page + image_url: # (optional) This is a link to a thumbnail image depicting the feature + video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg diff --git a/data/deprecations/14-8-sast-analyzer-removals.yml b/data/deprecations/14-8-sast-analyzer-removals.yml new file mode 100644 index 00000000000..6bbeee0cd6b --- /dev/null +++ b/data/deprecations/14-8-sast-analyzer-removals.yml @@ -0,0 +1,32 @@ +- name: "SAST analyzer consolidation and CI/CD template changes" + announcement_milestone: "14.8" + announcement_date: "2022-02-22" + removal_milestone: "15.0" + removal_date: "2022-05-22" + breaking_change: true + reporter: connorgilbert + body: | # Do not modify this line, instead modify the lines below. + GitLab SAST uses various [analyzers](https://docs.gitlab.com/ee/user/application_security/sast/analyzers/) to scan code for vulnerabilities. + + We are reducing the number of analyzers used in GitLab SAST as part of our long-term strategy to deliver a better and more consistent user experience. + Streamlining the set of analyzers will also enable faster [iteration](https://about.gitlab.com/handbook/values/#iteration), better [results](https://about.gitlab.com/handbook/values/#results), and greater [efficiency](https://about.gitlab.com/handbook/values/#results) (including a reduction in CI runner usage in most cases). + + In GitLab 15.0, GitLab SAST will no longer use the following analyzers: + + - [ESLint](https://gitlab.com/gitlab-org/security-products/analyzers/eslint) (JavaScript, TypeScript, React) + - [Gosec](https://gitlab.com/gitlab-org/security-products/analyzers/gosec) (Go) + - [Bandit](https://gitlab.com/gitlab-org/security-products/analyzers/bandit) (Python) + + These analyzers will be removed from the [GitLab-managed SAST CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml) and replaced with the [Semgrep-based analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep). + They will no longer receive routine updates, except for security issues. + We will not delete container images previously published for these analyzers; any such change would be announced as a [deprecation, removal, or breaking change announcement](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes). + + We will also remove Java from the scope of the [SpotBugs](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) analyzer and replace it with the [Semgrep-based analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep). + This change will make it simpler to scan Java code; compilation will no longer be required. + This change will be reflected in the automatic language detection portion of the [GitLab-managed SAST CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml). + + If you applied customizations to any of the affected analyzers, you must take action as detailed in the [deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352554#breaking-change). +# The following items are not published on the docs page, but may be used in the future. + stage: Secure + tiers: [Free, Silver, Gold, Core, Premium, Ultimate] + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352554 diff --git a/data/deprecations/14-8-sast-dotnet-21.yml b/data/deprecations/14-8-sast-dotnet-21.yml new file mode 100644 index 00000000000..ab1b3c16b23 --- /dev/null +++ b/data/deprecations/14-8-sast-dotnet-21.yml @@ -0,0 +1,31 @@ +- name: "SAST support for .NET 2.1" + announcement_milestone: "14.8" + announcement_date: "2022-02-22" + removal_milestone: "15.0" + removal_date: "2022-05-22" + breaking_change: true + reporter: connorgilbert + body: | # Do not modify this line, instead modify the lines below. + The GitLab SAST Security Code Scan analyzer scans .NET code for security vulnerabilities. + For technical reasons, the analyzer must first build the code to scan it. + + In GitLab versions prior to 15.0, the default analyzer image (version 2) includes support for: + + - .NET 2.1 + - .NET 3.0 and .NET Core 3.0 + - .NET Core 3.1 + - .NET 5.0 + + In GitLab 15.0, we will change the default major version for this analyzer from version 2 to version 3. This change: + + - Adds [severity values for vulnerabilities](https://gitlab.com/gitlab-org/gitlab/-/issues/350408) along with [other new features and improvements](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan/-/blob/master/CHANGELOG.md). + - Removes .NET 2.1 support. + - Adds support for .NET 6.0, Visual Studio 2019, and Visual Studio 2022. + + Version 3 was [announced in GitLab 14.6](https://about.gitlab.com/releases/2021/12/22/gitlab-14-6-released/#sast-support-for-net-6) and made available as an optional upgrade. + + If you rely on .NET 2.1 support being present in the analyzer image by default, you must take action as detailed in the [deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352553#breaking-change). +# The following items are not published on the docs page, but may be used in the future. + stage: Secure + tiers: [Free, Silver, Gold, Core, Premium, Ultimate] + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352553 diff --git a/data/deprecations/14-8-secret-detection-configurations.yml b/data/deprecations/14-8-secret-detection-configurations.yml new file mode 100644 index 00000000000..f9103c1cea3 --- /dev/null +++ b/data/deprecations/14-8-secret-detection-configurations.yml @@ -0,0 +1,28 @@ +- name: "Secret Detection configuration variables deprecated" + announcement_milestone: "14.8" + announcement_date: "2022-02-22" + removal_milestone: "15.0" + removal_date: "2022-05-22" + breaking_change: false + reporter: connorgilbert + body: | # Do not modify this line, instead modify the lines below. + To make it simpler and more reliable to [customize GitLab Secret Detection](https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings), we're deprecating some of the variables that you could previously set in your CI/CD configuration. + + The following variables currently allow you to customize the options for historical scanning, but interact poorly with the [GitLab-managed CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml) and are now deprecated: + + - `SECRET_DETECTION_COMMIT_FROM` + - `SECRET_DETECTION_COMMIT_TO` + - `SECRET_DETECTION_COMMITS` + - `SECRET_DETECTION_COMMITS_FILE` + + The `SECRET_DETECTION_ENTROPY_LEVEL` previously allowed you to configure rules that only considered the entropy level of strings in your codebase, and is now deprecated. + This type of entropy-only rule created an unacceptable number of incorrect results (false positives) and is no longer supported. + + In GitLab 15.0, we'll update the Secret Detection [analyzer](https://docs.gitlab.com/ee/user/application_security/terminology/#analyzer) to ignore these deprecated options. + You'll still be able to configure historical scanning of your commit history by setting the [`SECRET_DETECTION_HISTORIC_SCAN` CI/CD variable](https://docs.gitlab.com/ee/user/application_security/secret_detection/#available-cicd-variables). + + For further details, see [the deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352565). +# The following items are not published on the docs page, but may be used in the future. + stage: Secure + tiers: [Free, Silver, Gold, Core, Premium, Ultimate] + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352565 diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 037890b93bf..49eba783e21 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -867,6 +867,68 @@ To align with this change, API calls to list external status checks will also re **Planned removal milestone: 15.0 (2022-05-22)** +### GraphQL ID and GlobalID compatibility + +WARNING: +This feature will be changed or removed in 15.0 +as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Before updating GitLab, review the details carefully to determine if you need to make any +changes to your code, settings, or workflow. + +We are removing a non-standard extension to our GraphQL processor, which we added for backwards compatibility. This extension modifies the validation of GraphQL queries, allowing the use of the `ID` type for arguments where it would normally be rejected. +Some arguments originally had the type `ID`. These were changed to specific +kinds of `ID`. This change may be a breaking change if you: + +- Use GraphQL. +- Use the `ID` type for any argument in your query signatures. + +Some field arguments still have the `ID` type. These are typically for +IID values, or namespace paths. An example is `Query.project(fullPath: ID!)`. + +For a list of affected and unaffected field arguments, +see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/352832). + +You can test if this change affects you by validating +your queries locally, using schema data fetched from a GitLab server. +You can do this by using the GraphQL explorer tool for the relevant GitLab +instance. For example: `https://gitlab.com/-/graphql-explorer`. + +For example, the following query illustrates the breaking change: + +```graphql +# a query using the deprecated type of Query.issue(id:) +# WARNING: This will not work after GitLab 15.0 +query($id: ID!) { + deprecated: issue(id: $id) { + title, description + } +} +``` + +The query above will not work after GitLab 15.0 is released, because the type +of `Query.issue(id:)` is actually `IssueID!`. + +Instead, you should use one of the following two forms: + +```graphql +# This will continue to work +query($id: IssueID!) { + a: issue(id: $id) { + title, description + } + b: issue(id: "gid://gitlab/Issue/12345") { + title, description + } +} +``` + +This query works now, and will continue to work after GitLab 15.0. +You should convert any queries in the first form (using `ID` as a named type in the signature) +to one of the other two forms (using the correct appropriate type in the signature, or using +an inline argument expression). + +**Planned removal milestone: 15.0 (2022-04-22)** + ### OAuth tokens without expiration WARNING: @@ -1082,6 +1144,88 @@ If you have explicitly excluded retire.js using DS_EXCLUDED_ANALYZERS you will n **Planned removal milestone: 15.0 (2022-05-22)** +### SAST analyzer consolidation and CI/CD template changes + +WARNING: +This feature will be changed or removed in 15.0 +as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Before updating GitLab, review the details carefully to determine if you need to make any +changes to your code, settings, or workflow. + +GitLab SAST uses various [analyzers](https://docs.gitlab.com/ee/user/application_security/sast/analyzers/) to scan code for vulnerabilities. + +We are reducing the number of analyzers used in GitLab SAST as part of our long-term strategy to deliver a better and more consistent user experience. +Streamlining the set of analyzers will also enable faster [iteration](https://about.gitlab.com/handbook/values/#iteration), better [results](https://about.gitlab.com/handbook/values/#results), and greater [efficiency](https://about.gitlab.com/handbook/values/#results) (including a reduction in CI runner usage in most cases). + +In GitLab 15.0, GitLab SAST will no longer use the following analyzers: + +- [ESLint](https://gitlab.com/gitlab-org/security-products/analyzers/eslint) (JavaScript, TypeScript, React) +- [Gosec](https://gitlab.com/gitlab-org/security-products/analyzers/gosec) (Go) +- [Bandit](https://gitlab.com/gitlab-org/security-products/analyzers/bandit) (Python) + +These analyzers will be removed from the [GitLab-managed SAST CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml) and replaced with the [Semgrep-based analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep). +They will no longer receive routine updates, except for security issues. +We will not delete container images previously published for these analyzers; any such change would be announced as a [deprecation, removal, or breaking change announcement](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-removals-and-breaking-changes). + +We will also remove Java from the scope of the [SpotBugs](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) analyzer and replace it with the [Semgrep-based analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep). +This change will make it simpler to scan Java code; compilation will no longer be required. +This change will be reflected in the automatic language detection portion of the [GitLab-managed SAST CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml). + +If you applied customizations to any of the affected analyzers, you must take action as detailed in the [deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352554#breaking-change). + +**Planned removal milestone: 15.0 (2022-05-22)** + +### SAST support for .NET 2.1 + +WARNING: +This feature will be changed or removed in 15.0 +as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Before updating GitLab, review the details carefully to determine if you need to make any +changes to your code, settings, or workflow. + +The GitLab SAST Security Code Scan analyzer scans .NET code for security vulnerabilities. +For technical reasons, the analyzer must first build the code to scan it. + +In GitLab versions prior to 15.0, the default analyzer image (version 2) includes support for: + +- .NET 2.1 +- .NET 3.0 and .NET Core 3.0 +- .NET Core 3.1 +- .NET 5.0 + +In GitLab 15.0, we will change the default major version for this analyzer from version 2 to version 3. This change: + +- Adds [severity values for vulnerabilities](https://gitlab.com/gitlab-org/gitlab/-/issues/350408) along with [other new features and improvements](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan/-/blob/master/CHANGELOG.md). +- Removes .NET 2.1 support. +- Adds support for .NET 6.0, Visual Studio 2019, and Visual Studio 2022. + +Version 3 was [announced in GitLab 14.6](https://about.gitlab.com/releases/2021/12/22/gitlab-14-6-released/#sast-support-for-net-6) and made available as an optional upgrade. + +If you rely on .NET 2.1 support being present in the analyzer image by default, you must take action as detailed in the [deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352553#breaking-change). + +**Planned removal milestone: 15.0 (2022-05-22)** + +### Secret Detection configuration variables deprecated + +To make it simpler and more reliable to [customize GitLab Secret Detection](https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings), we're deprecating some of the variables that you could previously set in your CI/CD configuration. + +The following variables currently allow you to customize the options for historical scanning, but interact poorly with the [GitLab-managed CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml) and are now deprecated: + +- `SECRET_DETECTION_COMMIT_FROM` +- `SECRET_DETECTION_COMMIT_TO` +- `SECRET_DETECTION_COMMITS` +- `SECRET_DETECTION_COMMITS_FILE` + +The `SECRET_DETECTION_ENTROPY_LEVEL` previously allowed you to configure rules that only considered the entropy level of strings in your codebase, and is now deprecated. +This type of entropy-only rule created an unacceptable number of incorrect results (false positives) and is no longer supported. + +In GitLab 15.0, we'll update the Secret Detection [analyzer](https://docs.gitlab.com/ee/user/application_security/terminology/#analyzer) to ignore these deprecated options. +You'll still be able to configure historical scanning of your commit history by setting the [`SECRET_DETECTION_HISTORIC_SCAN` CI/CD variable](https://docs.gitlab.com/ee/user/application_security/secret_detection/#available-cicd-variables). + +For further details, see [the deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352565). + +**Planned removal milestone: 15.0 (2022-05-22)** + ### Support for gRPC-aware proxy deployed between Gitaly and rest of GitLab WARNING: diff --git a/doc/user/permissions.md b/doc/user/permissions.md index a00ac13c87b..e45bce4b24f 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -150,6 +150,7 @@ The following table lists project permissions available for each role: | [Projects](project/index.md):<br>Export project | | | | ✓ | ✓ | | [Projects](project/index.md):<br>Manage [project access tokens](project/settings/project_access_tokens.md) **(FREE SELF)** **(PREMIUM SAAS)** (*11*) | | | | ✓ | ✓ | | [Projects](project/index.md):<br>Manage [Project Operations](../operations/index.md) | | | | ✓ | ✓ | +| [Projects](project/index.md):<br>Rename project | | | | ✓ | ✓ | | [Projects](project/index.md):<br>Share (invite) projects with groups | | | | ✓ (*7*) | ✓ (*7*) | | [Projects](project/index.md):<br>View 2FA status of members | | | | ✓ | ✓ | | [Projects](project/index.md):<br>Administer project compliance frameworks | | | | | ✓ | @@ -157,7 +158,6 @@ The following table lists project permissions available for each role: | [Projects](project/index.md):<br>Change project visibility level | | | | | ✓ | | [Projects](project/index.md):<br>Delete project | | | | | ✓ | | [Projects](project/index.md):<br>Disable notification emails | | | | | ✓ | -| [Projects](project/index.md):<br>Rename project | | | | | ✓ | | [Projects](project/index.md):<br>Transfer project to another namespace | | | | | ✓ | | [Repository](project/repository/index.md):<br>Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | [Repository](project/repository/index.md):<br>View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb index 04f53accfeb..883abef9bdb 100644 --- a/lib/gitlab/github_import/representation/diff_note.rb +++ b/lib/gitlab/github_import/representation/diff_note.rb @@ -129,17 +129,7 @@ module Gitlab def discussion_id strong_memoize(:discussion_id) do - if in_reply_to_id.present? - current_discussion_id - else - Discussion.discussion_id( - Struct - .new(:noteable_id, :noteable_type) - .new(merge_request.id, NOTEABLE_TYPE) - ).tap do |discussion_id| - cache_discussion_id(discussion_id) - end - end + (in_reply_to_id.present? && current_discussion_id) || generate_discussion_id end end @@ -160,6 +150,16 @@ module Gitlab side == 'RIGHT' end + def generate_discussion_id + Discussion.discussion_id( + Struct + .new(:noteable_id, :noteable_type) + .new(merge_request.id, NOTEABLE_TYPE) + ).tap do |discussion_id| + cache_discussion_id(discussion_id) + end + end + def cache_discussion_id(discussion_id) Gitlab::Cache::Import::Caching.write(discussion_id_cache_key(note_id), discussion_id) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2f3fad4e703..8769f0fcede 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12869,6 +12869,9 @@ msgstr "" msgid "Display name" msgstr "" +msgid "Display progress of child issues" +msgstr "" + msgid "Display rendered file" msgstr "" diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index ef634d3ccda..8cf1fa0705f 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -32,6 +32,9 @@ module QA # Given *gitlab_address* = 'http://gitlab-abc123.test/' #=> http://about.gitlab-abc123.test/ Runtime::Scenario.define(:about_address, URI(-> { gitlab_address.host = "about.#{gitlab_address.host}"; gitlab_address }.call).to_s) # rubocop:disable Style/Semicolon + # Save the scenario class name + Runtime::Scenario.define(:klass, self.class.name) + ## # Setup knapsack and download latest report # diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index 2c9e302fc56..a861c13a44c 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -7,6 +7,7 @@ module QA module Specs class Runner < Scenario::Template attr_accessor :tty, :tags, :options + RegexMismatchError = Class.new(StandardError) DEFAULT_TEST_PATH_ARGS = ['--', File.expand_path('./features', __dir__)].freeze DEFAULT_STD_ARGS = [$stderr, $stdout].freeze @@ -72,16 +73,48 @@ module QA elsif Runtime::Scenario.attributes[:count_examples_only] args.unshift('--dry-run') out = StringIO.new + RSpec::Core::Runner.run(args.flatten, $stderr, out).tap do |status| abort if status.nonzero? end - $stdout.puts out.string.match(/(\d+) examples,/)[1] + + begin + total_examples = out.string.match(/(\d+) examples?,/)[1] + rescue StandardError + raise RegexMismatchError, 'Rspec output did not match regex' + end + + filename = build_filename + + File.open(filename, 'w') { |f| f.write(total_examples) } if total_examples.to_i > 0 + + $stdout.puts "Total examples in #{Runtime::Scenario.klass}: #{total_examples}#{total_examples.to_i > 0 ? ". Saved to file: #{filename}" : ''}" else RSpec::Core::Runner.run(args.flatten, *DEFAULT_STD_ARGS).tap do |status| abort if status.nonzero? end end end + + private + + def build_filename + filename = Runtime::Scenario.klass.split('::').last(3).join('_').downcase + + tags = [] + options.reduce do |before, after| + tags << after if %w[--tag -t].include?(before) + after + end + tags = tags.compact.join('_') + + filename.concat("_#{tags}") unless tags.empty? + + filename.concat('.txt') + + FileUtils.mkdir_p('no_of_examples') + File.join('no_of_examples', filename) + end end end end diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index 5cc9ff403cd..e52ca1fb17c 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -14,6 +14,7 @@ RSpec.describe QA::Specs::Runner do allow(QA::Runtime::Browser).to receive(:configure!) QA::Runtime::Scenario.define(:gitlab_address, "http://gitlab.test") + QA::Runtime::Scenario.define(:klass, "QA::Scenario::Test::Instance::All") end it_behaves_like 'excludes orchestrated, transient, and geo' @@ -43,6 +44,43 @@ RSpec.describe QA::Specs::Runner do subject.perform end + it 'writes to file when examples are more than zero' do + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect(File).to receive(:open).with('no_of_examples/test_instance_all.txt', 'w') { '22' } + + subject.perform + end + + it 'does not write to file when zero examples' do + out.string = '0 examples,' + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect(File).not_to receive(:open) + + subject.perform + end + + it 'raises error when Rspec output does not match regex' do + out.string = '0' + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect { subject.perform } + .to raise_error(QA::Specs::Runner::RegexMismatchError, 'Rspec output did not match regex') + end + + context 'when --tag is specified as an option' do + subject { described_class.new.tap { |runner| runner.options = %w[--tag actioncable] } } + + it 'includes the option value in the file name' do + expect_rspec_runner_arguments(['--dry-run', '--tag', '~geo', '--tag', 'actioncable', *described_class::DEFAULT_TEST_PATH_ARGS], [$stderr, anything]) + + expect(File).to receive(:open).with('no_of_examples/test_instance_all_actioncable.txt', 'w') { '22' } + + subject.perform + end + end + after do QA::Runtime::Scenario.attributes.delete(:count_examples_only) end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 58d34a5e5c1..0f1501d4c3c 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -397,9 +397,10 @@ RSpec.describe SearchController do expect(payload[:metadata]['meta.search.filters.confidential']).to eq('true') expect(payload[:metadata]['meta.search.filters.state']).to eq('true') expect(payload[:metadata]['meta.search.project_ids']).to eq(%w(456 789)) + expect(payload[:metadata]['meta.search.search_level']).to eq('multi-project') end - get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', project_ids: %w(456 789), confidential: true, state: true, force_search_results: true } + get :show, params: { scope: 'issues', search: 'hello world', group_id: '123', project_id: '456', project_ids: %w(456 789), search_level: 'multi-project', confidential: true, state: true, force_search_results: true } end it 'appends the default scope in meta.search.scope' do diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 51e91b76d3f..b3fbf5d356e 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -404,4 +404,47 @@ RSpec.describe 'New project', :js do end end end + + context 'from Bitbucket', :js do + shared_examples 'has a link to bitbucket cloud' do + context 'when bitbucket is not configured' do + before do + allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_call_original + allow(Gitlab::Auth::OAuth::Provider) + .to receive(:enabled?).with(:bitbucket) + .and_return(false) + + visit new_project_path + click_link 'Import project' + click_link 'Bitbucket Cloud' + end + + it 'shows import instructions' do + expect(find('.modal-body')).to have_content(bitbucket_link_content) + end + end + end + + context 'as a user' do + let(:user) { create(:user) } + let(:bitbucket_link_content) { 'To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration' } + + before do + sign_in(user) + end + + it_behaves_like 'has a link to bitbucket cloud' + end + + context 'as an admin' do + let(:user) { create(:admin) } + let(:bitbucket_link_content) { 'To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration' } + + before do + sign_in(user) + end + + it_behaves_like 'has a link to bitbucket cloud' + end + end end diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js index d2d1fe6b2d8..0cef18c60de 100644 --- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js @@ -31,6 +31,10 @@ describe('Design reply form component', () => { }); } + beforeEach(() => { + gon.features = { markdownContinueLists: true }; + }); + afterEach(() => { wrapper.destroy(); }); diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index 3043c4c3673..dd511c3945c 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -25,6 +25,7 @@ describe('Description field component', () => { beforeEach(() => { jest.spyOn(eventHub, '$emit'); + gon.features = { markdownContinueLists: true }; }); afterEach(() => { diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js index d19f9352bbc..e06d1384610 100644 --- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js +++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js @@ -6,11 +6,13 @@ describe('Confirm Modal', () => { let wrapper; let modal; - const createComponent = ({ primaryText, primaryVariant } = {}) => { + const createComponent = ({ primaryText, primaryVariant, title, hideCancel = false } = {}) => { wrapper = mount(ConfirmModal, { propsData: { primaryText, primaryVariant, + hideCancel, + title, }, }); }; @@ -55,5 +57,19 @@ describe('Confirm Modal', () => { expect(customProps.text).toBe('OK'); expect(customProps.attributes.variant).toBe('confirm'); }); + + it('should hide the cancel button if `hideCancel` is set', () => { + createComponent({ hideCancel: true }); + const props = findGlModal().props(); + + expect(props.actionCancel).toBeNull(); + }); + + it('should set the modal title when the `title` prop is set', () => { + const title = 'Modal title'; + createComponent({ title }); + + expect(findGlModal().props().title).toBe(title); + }); }); }); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index ab81ec47b64..dded32cc890 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -165,6 +165,80 @@ describe('init markdown', () => { // cursor placement should be between tags expect(textArea.selectionStart).toBe(start.length + tag.length); }); + + describe('Continuing markdown lists', () => { + const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' }); + + beforeEach(() => { + gon.features = { markdownContinueLists: true }; + }); + + it.each` + text | expected + ${'- item'} | ${'- item\n- '} + ${'- [ ] item'} | ${'- [ ] item\n- [ ] '} + ${'- [x] item'} | ${'- [x] item\n- [x] '} + ${'- item\n - second'} | ${'- item\n - second\n - '} + ${'1. item'} | ${'1. item\n1. '} + ${'1. [ ] item'} | ${'1. [ ] item\n1. [ ] '} + ${'1. [x] item'} | ${'1. [x] item\n1. [x] '} + ${'108. item'} | ${'108. item\n108. '} + ${'108. item\n - second'} | ${'108. item\n - second\n - '} + ${'108. item\n 1. second'} | ${'108. item\n 1. second\n 1. '} + `('adds correct list continuation characters', ({ text, expected }) => { + textArea.value = text; + textArea.setSelectionRange(text.length, text.length); + + textArea.addEventListener('keydown', keypressNoteText); + textArea.dispatchEvent(enterEvent); + + expect(textArea.value).toEqual(expected); + expect(textArea.selectionStart).toBe(expected.length); + }); + + // test that when pressing Enter on an empty list item, the empty + // list item text is selected, so that when the Enter propagates, + // it's removed + it.each` + text | expected + ${'- item\n- '} | ${'- item\n'} + ${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'} + ${'- [x] item\n- [x] '} | ${'- [x] item\n'} + ${'- item\n - second\n - '} | ${'- item\n - second\n'} + ${'1. item\n1. '} | ${'1. item\n'} + ${'1. [ ] item\n1. [ ] '} | ${'1. [ ] item\n'} + ${'1. [x] item\n1. [x] '} | ${'1. [x] item\n'} + ${'108. item\n108. '} | ${'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 }) => { + textArea.value = text; + textArea.setSelectionRange(text.length, text.length); + + textArea.addEventListener('keydown', keypressNoteText); + textArea.dispatchEvent(enterEvent); + + expect(textArea.value.substr(0, textArea.selectionStart)).toEqual(expected); + expect(textArea.selectionStart).toBe(expected.length); + expect(textArea.selectionEnd).toBe(text.length); + }); + + it('does nothing if feature flag disabled', () => { + gon.features = { markdownContinueLists: false }; + + const text = '- item'; + const expected = '- item'; + + textArea.value = text; + textArea.setSelectionRange(text.length, text.length); + + textArea.addEventListener('keydown', keypressNoteText); + textArea.dispatchEvent(enterEvent); + + expect(textArea.value).toEqual(expected); + expect(textArea.selectionStart).toBe(expected.length); + }); + }); }); describe('with selection', () => { diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index d3b5ab02f24..3e80b24f128 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -45,6 +45,8 @@ describe('issue_note_form component', () => { noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', noteId: '545', }; + + gon.features = { markdownContinueLists: true }; }); afterEach(() => { diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js index d3e484cf913..b79dc0bf976 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -36,6 +36,7 @@ describe('IssuableEditForm', () => { beforeEach(() => { wrapper = createComponent(); + gon.features = { markdownContinueLists: true }; }); afterEach(() => { diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js index 13f221fd9d9..44684619fae 100644 --- a/spec/frontend/zen_mode_spec.js +++ b/spec/frontend/zen_mode_spec.js @@ -45,6 +45,8 @@ describe('ZenMode', () => { // Set this manually because we can't actually scroll the window zen.scroll_position = 456; + + gon.features = { markdownContinueLists: true }; }); describe('enabling dropzone', () => { diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 1d78778c757..604ce0fe0c1 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1026,4 +1026,26 @@ RSpec.describe ProjectsHelper do end end end + + describe '#import_from_bitbucket_message' do + before do + allow(helper).to receive(:current_user).and_return(user) + end + + context 'as a user' do + it 'returns a link to contact an administrator' do + allow(user).to receive(:admin?).and_return(false) + + expect(helper.import_from_bitbucket_message).to have_text('To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration') + end + end + + context 'as an administrator' do + it 'returns a link to configure bitbucket' do + allow(user).to receive(:admin?).and_return(true) + + expect(helper.import_from_bitbucket_message).to have_text('To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration') + end + end + end end diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb index 63834cfdb94..fe3040c102b 100644 --- a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb +++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_redis_shared_state do +RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_redis_cache do let(:hunk) do '@@ -1 +1 @@ -Hello @@ -166,6 +166,23 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote, :clean_gitlab_red expect(new_discussion_note.discussion_id) .to eq('SECOND_DISCUSSION_ID') end + + context 'when cached value does not exist' do + it 'falls back to generating a new discussion_id' do + expect(Discussion) + .to receive(:discussion_id) + .and_return('NEW_DISCUSSION_ID') + + reply_note = described_class.from_json_hash( + 'note_id' => note.note_id + 1, + 'in_reply_to_id' => note.note_id + ) + reply_note.project = project + reply_note.merge_request = merge_request + + expect(reply_note.discussion_id).to eq('NEW_DISCUSSION_ID') + end + end end end diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 85f433f5f81..0d65fe302e1 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -16,7 +16,7 @@ RSpec.describe ServiceHook do let(:data) { { key: 'value' } } it '#execute' do - expect(WebHookService).to receive(:new).with(hook, data, 'service_hook').and_call_original + expect(WebHookService).to receive(:new).with(hook, data, 'service_hook', force: false).and_call_original expect_any_instance_of(WebHookService).to receive(:execute) hook.execute(data) diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 89bfb742f5d..a3d36058b74 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -168,17 +168,17 @@ RSpec.describe SystemHook do let(:data) { { key: 'value' } } let(:hook_name) { 'system_hook' } - before do - expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original - end - it '#execute' do + expect(WebHookService).to receive(:new).with(hook, data, hook_name, force: false).and_call_original + expect_any_instance_of(WebHookService).to receive(:execute) hook.execute(data, hook_name) end it '#async_execute' do + expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original + expect_any_instance_of(WebHookService).to receive(:async_execute) hook.async_execute(data, hook_name) diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index c292e78b32d..482e372543c 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -100,12 +100,18 @@ RSpec.describe WebHook do hook.execute(data, hook_name) end - it 'does not execute non-executable hooks' do - hook.update!(disabled_until: 1.day.from_now) + it 'passes force: false to the web hook service by default' do + expect(WebHookService) + .to receive(:new).with(hook, data, hook_name, force: false).and_return(double(execute: :done)) - expect(WebHookService).not_to receive(:new) + expect(hook.execute(data, hook_name)).to eq :done + end - hook.execute(data, hook_name) + it 'passes force: true to the web hook service if required' do + expect(WebHookService) + .to receive(:new).with(hook, data, hook_name, force: true).and_return(double(execute: :forced)) + + expect(hook.execute(data, hook_name, force: true)).to eq :forced end it '#async_execute' do diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb index cd6284b4a87..d97a6f15270 100644 --- a/spec/services/test_hooks/project_service_spec.rb +++ b/spec/services/test_hooks/project_service_spec.rb @@ -37,7 +37,7 @@ RSpec.describe TestHooks::ProjectService do it 'executes hook' do allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -49,7 +49,7 @@ RSpec.describe TestHooks::ProjectService do it 'executes hook' do allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -69,7 +69,7 @@ RSpec.describe TestHooks::ProjectService do allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data) allow_next(NotesFinder).to receive(:execute).and_return(Note.all) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -86,7 +86,7 @@ RSpec.describe TestHooks::ProjectService do allow(issue).to receive(:to_hook_data).and_return(sample_data) allow_next(IssuesFinder).to receive(:execute).and_return([issue]) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -119,7 +119,7 @@ RSpec.describe TestHooks::ProjectService do allow(merge_request).to receive(:to_hook_data).and_return(sample_data) allow_next(MergeRequestsFinder).to receive(:execute).and_return([merge_request]) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -138,7 +138,7 @@ RSpec.describe TestHooks::ProjectService do allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data) allow_next(Ci::JobsFinder).to receive(:execute).and_return([ci_job]) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -157,7 +157,7 @@ RSpec.describe TestHooks::ProjectService do allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data) allow_next(Ci::PipelinesFinder).to receive(:execute).and_return([pipeline]) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -184,7 +184,7 @@ RSpec.describe TestHooks::ProjectService do create(:wiki_page, wiki: project.wiki) allow(Gitlab::DataBuilder::WikiPage).to receive(:build).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -203,7 +203,7 @@ RSpec.describe TestHooks::ProjectService do allow(release).to receive(:to_hook_data).and_return(sample_data) allow_next(ReleasesFinder).to receive(:execute).and_return([release]) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb index 48c8c24212a..66a1218d123 100644 --- a/spec/services/test_hooks/system_service_spec.rb +++ b/spec/services/test_hooks/system_service_spec.rb @@ -32,7 +32,7 @@ RSpec.describe TestHooks::SystemService do it 'executes hook' do expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original - expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -45,7 +45,7 @@ RSpec.describe TestHooks::SystemService do allow(project.repository).to receive(:tags).and_return(['tag']) expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original - expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -57,7 +57,7 @@ RSpec.describe TestHooks::SystemService do it 'executes hook' do expect(Gitlab::DataBuilder::Repository).to receive(:sample_data).and_call_original - expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end @@ -76,7 +76,7 @@ RSpec.describe TestHooks::SystemService do it 'executes hook' do expect(MergeRequest).to receive(:of_projects).and_return([merge_request]) expect(merge_request).to receive(:to_hook_data).and_return(sample_data) - expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(hook).to receive(:execute).with(sample_data, trigger_key, force: true).and_return(success_result) expect(service.execute).to include(success_result) end end diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 448d00076c1..64371f97908 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -52,6 +52,25 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state end end + describe '#disabled?' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.new(hook, data, :push_hooks, force: forced) } + + let(:hook) { double(executable?: executable, allow_local_requests?: false) } + + where(:forced, :executable, :disabled) do + false | true | false + false | false | true + true | true | false + true | false | false + end + + with_them do + it { is_expected.to have_attributes(disabled?: disabled) } + end + end + describe '#execute' do let!(:uuid) { SecureRandom.uuid } let(:headers) do @@ -129,7 +148,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state end it 'does not execute disabled hooks' do - project_hook.update!(recent_failures: 4) + allow(service_instance).to receive(:disabled?).and_return(true) expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' }) end @@ -251,6 +270,20 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success') end + context 'when forced' do + let(:service_instance) { described_class.new(project_hook, data, :push_hooks, force: true) } + + it 'logs execution inline' do + expect(::WebHooks::LogExecutionWorker).not_to receive(:perform_async) + expect(::WebHooks::LogExecutionService) + .to receive(:new) + .with(hook: project_hook, log_data: Hash, response_category: :ok) + .and_return(double(execute: nil)) + + run_service + end + end + it 'log successful execution' do run_service diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb index 1a981f42086..2285d9a17e2 100644 --- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb @@ -85,7 +85,9 @@ RSpec.shared_examples 'User previews wiki changes' do end it 'renders content with CommonMark' do - fill_in :wiki_content, with: "1. one\n - sublist\n" + # using two `\n` ensures we're sublist to it's own line due + # to list auto-continue + fill_in :wiki_content, with: "1. one\n\n - sublist\n" click_on "Preview" # the above generates two separate lists (not embedded) in CommonMark |