From 5e555ebcf6ee2ce13e9956ae599fd811a79b4dbd Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 23 May 2022 18:08:14 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../diffs/components/diff_file.vue | 7 +- .../list/components/issues_list_app.vue | 16 +-- ...et_issues_counts_without_crm.query.graphql | 136 ------------------ .../get_issues_without_crm.query.graphql | 94 ------------ .../issues/show/components/description.vue | 22 ++- app/assets/javascripts/issues/show/utils.js | 50 +++---- .../pipelines_list/pipeline_url.vue | 3 +- .../assignees/assignee_avatar_link.vue | 6 +- .../components/source_viewer/constants.js | 4 + .../components/source_viewer/plugins/index.js | 13 ++ .../source_viewer/plugins/wrap_comments.js | 38 +++++ .../source_viewer/source_viewer.vue | 2 + .../stylesheets/page_bundles/issues_show.scss | 24 +++- app/assets/stylesheets/pages/note_form.scss | 13 +- app/models/namespace.rb | 2 +- app/serializers/issue_board_entity.rb | 9 ++ app/serializers/issue_entity.rb | 9 ++ app/services/git/branch_push_service.rb | 1 + app/services/merge_requests/base_service.rb | 5 +- .../merge_requests/create_pipeline_service.rb | 8 +- .../merge_requests/create_pipeline_worker.rb | 9 +- app/workers/update_merge_requests_worker.rb | 8 +- config/initializers/zz_metrics.rb | 4 +- config/sidekiq_queues.yml | 2 + doc/administration/audit_event_streaming.md | 4 +- .../packages/infrastructure_registry/index.md | 12 +- .../batched_background_migration_helpers.rb | 17 ++- .../reestablished_connection_stack.rb | 6 +- .../loggers/queue_duration_logger.rb | 15 +- .../projects/issues_controller_spec.rb | 9 +- spec/fixtures/api/schemas/entities/issue.json | 1 + .../api/schemas/entities/issue_board.json | 1 + .../list/components/issues_list_app_spec.js | 4 - spec/frontend/issues/show/utils_spec.js | 116 +++++++++++++-- .../assignees/assignee_avatar_link_spec.js | 24 ++++ .../source_viewer/plugins/index_spec.js | 14 ++ .../plugins/wrap_comments_spec.js | 20 +++ .../source_viewer/source_viewer_spec.js | 6 + .../background_migration_helpers_spec.rb | 2 +- ...tched_background_migration_helpers_spec.rb | 76 ++++++++-- .../reestablished_connection_stack_spec.rb | 2 +- .../loggers/queue_duration_logger_spec.rb | 12 +- spec/serializers/issue_board_entity_spec.rb | 6 + spec/serializers/issue_entity_spec.rb | 6 + .../create_pipeline_service_spec.rb | 13 ++ .../merge_requests/refresh_service_spec.rb | 15 ++ ...lished_connection_stack_shared_examples.rb | 2 +- .../create_pipeline_worker_spec.rb | 37 ++++- .../update_merge_requests_worker_spec.rb | 49 +++++-- 49 files changed, 555 insertions(+), 399 deletions(-) delete mode 100644 app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql delete mode 100644 app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql create mode 100644 app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js create mode 100644 app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js create mode 100644 spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js create mode 100644 spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 0b82be7140c..aec608007d5 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -456,12 +456,7 @@ export default {

{{ $options.i18n.autoCollapsed }}

- + {{ $options.i18n.expand }} diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index 9d8b339e813..2da6a049703 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -13,8 +13,6 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue'; import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; -import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql'; -import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql'; import createFlash, { FLASH_TYPES } from '~/flash'; import { TYPE_USER } from '~/graphql_shared/constants'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -160,9 +158,7 @@ export default { }, apollo: { issues: { - query() { - return this.hasCrmParameter ? getIssuesQuery : getIssuesWithoutCrmQuery; - }, + query: getIssuesQuery, variables() { return this.queryVariables; }, @@ -186,9 +182,7 @@ export default { debounce: 200, }, issuesCounts: { - query() { - return this.hasCrmParameter ? getIssuesCountsQuery : getIssuesCountsWithoutCrmQuery; - }, + query: getIssuesCountsQuery, variables() { return this.queryVariables; }, @@ -403,12 +397,6 @@ export default { page_before: this.pageParams.beforeCursor, }; }, - hasCrmParameter() { - return ( - window.location.search.includes('crm_contact_id=') || - window.location.search.includes('crm_organization_id=') - ); - }, }, watch: { $route(newValue, oldValue) { diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql deleted file mode 100644 index ab91aab1218..00000000000 --- a/app/assets/javascripts/issues/list/queries/get_issues_counts_without_crm.query.graphql +++ /dev/null @@ -1,136 +0,0 @@ -query getIssuesCountWithoutCrm( - $isProject: Boolean = false - $fullPath: ID! - $iid: String - $search: String - $assigneeId: String - $assigneeUsernames: [String!] - $authorUsername: String - $confidential: Boolean - $labelName: [String] - $milestoneTitle: [String] - $milestoneWildcardId: MilestoneWildcardId - $myReactionEmoji: String - $releaseTag: [String!] - $releaseTagWildcardId: ReleaseTagWildcardId - $types: [IssueType!] - $not: NegatedIssueFilterInput -) { - group(fullPath: $fullPath) @skip(if: $isProject) { - id - openedIssues: issues( - includeSubgroups: true - state: opened - iid: $iid - search: $search - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - types: $types - not: $not - ) { - count - } - closedIssues: issues( - includeSubgroups: true - state: closed - iid: $iid - search: $search - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - types: $types - not: $not - ) { - count - } - allIssues: issues( - includeSubgroups: true - state: all - iid: $iid - search: $search - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - types: $types - not: $not - ) { - count - } - } - project(fullPath: $fullPath) @include(if: $isProject) { - id - openedIssues: issues( - state: opened - iid: $iid - search: $search - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - releaseTag: $releaseTag - releaseTagWildcardId: $releaseTagWildcardId - types: $types - not: $not - ) { - count - } - closedIssues: issues( - state: closed - iid: $iid - search: $search - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - releaseTag: $releaseTag - releaseTagWildcardId: $releaseTagWildcardId - types: $types - not: $not - ) { - count - } - allIssues: issues( - state: all - iid: $iid - search: $search - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - releaseTag: $releaseTag - releaseTagWildcardId: $releaseTagWildcardId - types: $types - not: $not - ) { - count - } - } -} diff --git a/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql deleted file mode 100644 index 4a8b1dfd618..00000000000 --- a/app/assets/javascripts/issues/list/queries/get_issues_without_crm.query.graphql +++ /dev/null @@ -1,94 +0,0 @@ -#import "~/graphql_shared/fragments/page_info.fragment.graphql" -#import "./issue.fragment.graphql" - -query getIssuesWithoutCrm( - $hideUsers: Boolean = false - $isProject: Boolean = false - $isSignedIn: Boolean = false - $fullPath: ID! - $iid: String - $search: String - $sort: IssueSort - $state: IssuableState - $assigneeId: String - $assigneeUsernames: [String!] - $authorUsername: String - $confidential: Boolean - $labelName: [String] - $milestoneTitle: [String] - $milestoneWildcardId: MilestoneWildcardId - $myReactionEmoji: String - $releaseTag: [String!] - $releaseTagWildcardId: ReleaseTagWildcardId - $types: [IssueType!] - $not: NegatedIssueFilterInput - $beforeCursor: String - $afterCursor: String - $firstPageSize: Int - $lastPageSize: Int -) { - group(fullPath: $fullPath) @skip(if: $isProject) { - id - issues( - includeSubgroups: true - iid: $iid - search: $search - sort: $sort - state: $state - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - types: $types - not: $not - before: $beforeCursor - after: $afterCursor - first: $firstPageSize - last: $lastPageSize - ) { - pageInfo { - ...PageInfo - } - nodes { - ...IssueFragment - reference(full: true) - } - } - } - project(fullPath: $fullPath) @include(if: $isProject) { - id - issues( - iid: $iid - search: $search - sort: $sort - state: $state - assigneeId: $assigneeId - assigneeUsernames: $assigneeUsernames - authorUsername: $authorUsername - confidential: $confidential - labelName: $labelName - milestoneTitle: $milestoneTitle - milestoneWildcardId: $milestoneWildcardId - myReactionEmoji: $myReactionEmoji - releaseTag: $releaseTag - releaseTagWildcardId: $releaseTagWildcardId - types: $types - not: $not - before: $beforeCursor - after: $afterCursor - first: $firstPageSize - last: $lastPageSize - ) { - pageInfo { - ...PageInfo - } - nodes { - ...IssueFragment - } - } - } -} diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 4f97458dcd1..daa1632c4aa 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -12,6 +12,7 @@ import Vue from 'vue'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import createFlash from '~/flash'; +import { IssuableType } from '~/issues/constants'; import { isPositiveInteger } from '~/lib/utils/number_utils'; import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { __, s__, sprintf } from '~/locale'; @@ -66,7 +67,7 @@ export default { issuableType: { type: String, required: false, - default: 'issue', + default: IssuableType.Issue, }, updateUrl: { type: String, @@ -177,7 +178,9 @@ export default { onError: this.taskListUpdateError.bind(this), }); - this.renderSortableLists(); + if (this.issuableType === IssuableType.Issue) { + this.renderSortableLists(); + } } }, renderSortableLists() { @@ -185,6 +188,10 @@ export default { const lists = document.querySelectorAll('.description ul, .description ol'); lists.forEach((list) => { + if (list.children.length <= 1) { + return; + } + Array.from(list.children).forEach((listItem) => { listItem.prepend(this.createDragIconElement()); this.addPointerEventListeners(listItem); @@ -211,13 +218,18 @@ export default { }, addPointerEventListeners(listItem) { const pointeroverListener = (event) => { - if (isDragging() || this.isUpdating) { + const dragIcon = event.target.closest('li').querySelector('.drag-icon'); + if (!dragIcon || isDragging() || this.isUpdating) { return; } - event.target.closest('li').querySelector('.drag-icon').style.visibility = 'visible'; // eslint-disable-line no-param-reassign + dragIcon.style.visibility = 'visible'; }; const pointeroutListener = (event) => { - event.target.closest('li').querySelector('.drag-icon').style.visibility = 'hidden'; // eslint-disable-line no-param-reassign + const dragIcon = event.target.closest('li').querySelector('.drag-icon'); + if (!dragIcon) { + return; + } + dragIcon.style.visibility = 'hidden'; }; // We use pointerover/pointerout instead of CSS so that when we hover over a diff --git a/app/assets/javascripts/issues/show/utils.js b/app/assets/javascripts/issues/show/utils.js index 60e66f59f92..05b06586362 100644 --- a/app/assets/javascripts/issues/show/utils.js +++ b/app/assets/javascripts/issues/show/utils.js @@ -1,39 +1,35 @@ import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility'; /** - * Get the index from sourcepos that represents the line of - * the description when the description is split by newline. + * Returns the start and end `sourcepos` rows, converted to zero-based numbering. * * @param {String} sourcepos Source position in format `23:3-23:14` - * @returns {Number} Index of description split by newline + * @returns {Array} Start and end `sourcepos` rows, zero-based numbered */ -const getDescriptionIndex = (sourcepos) => { - const [startRange] = sourcepos.split(HYPHEN); +const getSourceposRows = (sourcepos) => { + const [startRange, endRange] = sourcepos.split(HYPHEN); const [startRow] = startRange.split(COLON); - return startRow - 1; + const [endRow] = endRange.split(COLON); + return [startRow - 1, endRow - 1]; }; /** - * Given a `ul` or `ol` element containing a new sort order, this function performs - * a depth-first search to get the new sort order in the form of sourcepos indices. + * Given a `ul` or `ol` element containing a new sort order, this function returns + * an array of this new order which is derived from its list items' sourcepos values. * * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order - * @returns {Array} An array representing the new order of the list + * @returns {Array} A numerical array representing the new order of the list. + * The numbers represent the rows of the original markdown source. */ const getNewSourcePositions = (list) => { const newSourcePositions = []; - function pushPositionOfChildListItems(el) { - if (!el) { - return; + Array.from(list.children).forEach((listItem) => { + const [start, end] = getSourceposRows(listItem.dataset.sourcepos); + for (let i = start; i <= end; i += 1) { + newSourcePositions.push(i); } - if (el.tagName === 'LI') { - newSourcePositions.push(getDescriptionIndex(el.dataset.sourcepos)); - } - Array.from(el.children).forEach(pushPositionOfChildListItems); - } - - pushPositionOfChildListItems(list); + }); return newSourcePositions; }; @@ -56,17 +52,17 @@ const getNewSourcePositions = (list) => { * And a reordered list (due to dragging Item 2 into Item 1's position) like: * *
- * 
    - *
  • + *
      + *
    • * Item 2 - *
        - *
      • Item 3
      • - *
      • Item 4
      • + *
          + *
        • Item 3
        • + *
        • Item 4
        • *
        * *
      • Item 1
      • - *
      • Item 5
      • - *
          + *
        • Item 5
        • + *
        *
* * This function returns: @@ -87,7 +83,7 @@ const getNewSourcePositions = (list) => { */ export const convertDescriptionWithNewSort = (description, list) => { const descriptionLines = description.split(NEWLINE); - const startIndexOfList = getDescriptionIndex(list.dataset.sourcepos); + const [startIndexOfList] = getSourceposRows(list.dataset.sourcepos); getNewSourcePositions(list) .map((lineIndex) => descriptionLines[lineIndex]) diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 63c492c8bcd..09d588aaafd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -137,9 +137,8 @@ export default { class="gl-text-decoration-underline gl-text-blue-600! gl-mr-3" data-testid="pipeline-url-link" data-qa-selector="pipeline_url_link" + >#{{ pipeline[pipelineKey] }} - #{{ pipeline[pipelineKey] }} -
import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { IssuableType } from '~/issues/constants'; +import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; import { isUserBusy } from '~/set_status_modal/utils'; import AssigneeAvatar from './assignee_avatar.vue'; @@ -94,6 +95,9 @@ export default { assigneeUrl() { return this.user.web_url || this.user.webUrl; }, + assigneeId() { + return isGid(this.user.id) ? getIdFromGraphQLId(this.user.id) : this.user.id; + }, }, }; @@ -103,7 +107,7 @@ export default { diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js index bed6dd4d5c6..0d78530d878 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js +++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js @@ -134,3 +134,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip'; export const BIDI_CHAR_TOOLTIP = __( 'Potentially unwanted character detected: Unicode BiDi Control', ); + +export const HLJS_COMMENT_SELECTOR = 'hljs-comment'; + +export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight'; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js new file mode 100644 index 00000000000..c9f7e5508be --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js @@ -0,0 +1,13 @@ +import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants'; +import wrapComments from './wrap_comments'; + +/** + * Registers our plugins for Highlight.js + * + * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst + * + * @param {Object} hljs - the Highlight.js instance. + */ +export const registerPlugins = (hljs) => { + hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments }); +}; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js new file mode 100644 index 00000000000..4a18733de94 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js @@ -0,0 +1,38 @@ +import { HLJS_COMMENT_SELECTOR } from '../constants'; + +const createWrapper = (content) => { + const span = document.createElement('span'); + span.className = HLJS_COMMENT_SELECTOR; + span.innerHTML = content; + return span.outerHTML; +}; + +/** + * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class. + * This ensures that multi-line comments are rendered correctly in the GitLab UI. + * + * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst + * + * @param {Object} Result - an object that represents the highlighted result from Highlight.js + */ +export default (result) => { + if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return; + + let wrapComment = false; + + // eslint-disable-next-line no-param-reassign + result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work + .split('\n') + .map((lineContent) => { + if (lineContent.includes(HLJS_COMMENT_SELECTOR)) { + wrapComment = true; + return lineContent; + } + const line = wrapComment ? createWrapper(lineContent) : lineContent; + if (lineContent.includes('')) { + wrapComment = false; + } + return line; + }) + .join('\n'); +}; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index ed87a202b15..f819a9e5be2 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -5,6 +5,7 @@ import eventHub from '~/notes/event_hub'; import languageLoader from '~/content_editor/services/highlight_js_language_loader'; import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants'; import Chunk from './components/chunk.vue'; +import { registerPlugins } from './plugins/index'; /* * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code, @@ -111,6 +112,7 @@ export default { let detectedLanguage = language; let highlightedContent; if (this.hljs) { + registerPlugins(this.hljs); if (!detectedLanguage) { const hljsHighlightAuto = this.hljs.highlightAuto(content); highlightedContent = hljsHighlightAuto.value; diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss index 9873a0121c0..63b0bcc0c7f 100644 --- a/app/assets/stylesheets/page_bundles/issues_show.scss +++ b/app/assets/stylesheets/page_bundles/issues_show.scss @@ -3,8 +3,8 @@ .description { ul, ol { - /* We're changing list-style-position to inside because the default of outside - * doesn't move the negative margin to the left of the bullet. */ + /* We're changing list-style-position to inside because the default of + * outside doesn't move negative margin to the left of the bullet. */ list-style-position: inside; } @@ -21,6 +21,26 @@ inset-block-start: 0.3rem; inset-inline-start: 1rem; } + + /* The inside bullet aligns itself to the bottom, which we see when text to its right wraps. + * We fix this by aligning it to the top. Targeting ::marker doesn't seem to work. */ + > * { + vertical-align: top; + } + + /* The inside bullet is treated like an element inside the li element, so when we have a + * multi-paragraph list item, the text doesn't start on the right of the bullet because + * it is a block level p element. We make it inline to fix this. */ + > p:first-of-type { + display: inline-block; + max-width: calc(100% - 1.5rem); + } + + /* We fix the other paragraphs not indenting to the + * right of the bullet due to the inside bullet. */ + > :not(p:first-of-type):not(.drag-icon):not(.task-list-item-checkbox):not(.gfm-issue):not(.js-add-task) { + margin-inline-start: 1rem; + } } ul.task-list { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index fada49ca40d..4736d441a4b 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -55,15 +55,6 @@ box-shadow ease-in-out 0.15s; background-color: $white; - &.is-focused { - @include gl-focus; - - .comment-toolbar, - .nav-links { - border-color: $blue-300; - } - } - &.is-dropzone-hover { border-color: $green-500; box-shadow: 0 0 2px $black-transparent, @@ -80,6 +71,10 @@ @include gl-shadow-none; } } + + .comment-warning-wrapper:focus-within { + @include gl-focus; + } } .md-header .nav-links { diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 47b652431d3..9c3e337c0c2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -553,7 +553,7 @@ class Namespace < ApplicationRecord private def cluster_enabled_granted? - root_ancestor.cluster_enabled_grant.present? && (Gitlab.com? || Gitlab.dev_or_test_env?) + (Gitlab.com? || Gitlab.dev_or_test_env?) && root_ancestor.cluster_enabled_grant.present? end def certificate_based_clusters_enabled_ff? diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb index bcad28d6aad..ebd0f037160 100644 --- a/app/serializers/issue_board_entity.rb +++ b/app/serializers/issue_board_entity.rb @@ -3,6 +3,10 @@ class IssueBoardEntity < Grape::Entity include RequestAwareEntity + format_with(:upcase) do |item| + item.try(:upcase) + end + expose :id expose :iid expose :title @@ -51,6 +55,11 @@ class IssueBoardEntity < Grape::Entity expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue| project_labels_path(issue.project, format: :json, include_ancestor_groups: true) end + + expose :issue_type, + as: :type, + format_with: :upcase, + documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" } end IssueBoardEntity.prepend_mod_with('IssueBoardEntity') diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 852a2e62b7d..eba2c49bc2e 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -3,6 +3,10 @@ class IssueEntity < IssuableEntity include TimeTrackableEntity + format_with(:upcase) do |item| + item.try(:upcase) + end + expose :state expose :milestone_id expose :updated_by_id @@ -75,6 +79,11 @@ class IssueEntity < IssuableEntity expose :issue_email_participants do |issue| issue.issue_email_participants.map { |x| { email: x.email } } end + + expose :issue_type, + as: :type, + format_with: :upcase, + documentation: { type: "String", desc: "One of #{::WorkItems::Type.base_types.keys.map(&:upcase)}" } end IssueEntity.prepend_mod_with('IssueEntity') diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb index 3c27ad56ebb..91f14251608 100644 --- a/app/services/git/branch_push_service.rb +++ b/app/services/git/branch_push_service.rb @@ -39,6 +39,7 @@ module Git def enqueue_update_mrs return if params[:merge_request_branches]&.exclude?(branch_name) + # TODO: pass params[:push_options] to worker UpdateMergeRequestsWorker.perform_async( project.id, current_user.id, diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 44be254441d..350b37aba68 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -185,9 +185,12 @@ module MergeRequests def create_pipeline_for(merge_request, user, async: false) if async + # TODO: pass push_options to worker MergeRequests::CreatePipelineWorker.perform_async(project.id, user.id, merge_request.id) else - MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request) + MergeRequests::CreatePipelineService + .new(project: project, current_user: user, params: params.slice(:push_options)) + .execute(merge_request) end end diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb index 9d7f8393ba5..37c734613e7 100644 --- a/app/services/merge_requests/create_pipeline_service.rb +++ b/app/services/merge_requests/create_pipeline_service.rb @@ -9,9 +9,11 @@ module MergeRequests end def create_detached_merge_request_pipeline(merge_request) - Ci::CreatePipelineService.new(pipeline_project(merge_request), - current_user, - ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request)) + Ci::CreatePipelineService + .new(pipeline_project(merge_request), + current_user, + ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request), + push_options: params[:push_options]) .execute(:merge_request_event, merge_request: merge_request) end diff --git a/app/workers/merge_requests/create_pipeline_worker.rb b/app/workers/merge_requests/create_pipeline_worker.rb index ee42a3dee08..b40408cf647 100644 --- a/app/workers/merge_requests/create_pipeline_worker.rb +++ b/app/workers/merge_requests/create_pipeline_worker.rb @@ -15,7 +15,7 @@ module MergeRequests worker_resource_boundary :cpu idempotent! - def perform(project_id, user_id, merge_request_id) + def perform(project_id, user_id, merge_request_id, params = {}) project = Project.find_by_id(project_id) return unless project @@ -25,7 +25,12 @@ module MergeRequests merge_request = MergeRequest.find_by_id(merge_request_id) return unless merge_request - MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request) + push_options = params.with_indifferent_access[:push_options] + + MergeRequests::CreatePipelineService + .new(project: project, current_user: user, params: { push_options: push_options }) + .execute(merge_request) + merge_request.update_head_pipeline end end diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index 5c96257cb63..eb69c0eaba6 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -13,13 +13,17 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker weight 3 loggable_arguments 2, 3, 4 - def perform(project_id, user_id, oldrev, newrev, ref) + def perform(project_id, user_id, oldrev, newrev, ref, params = {}) project = Project.find_by_id(project_id) return unless project user = User.find_by_id(user_id) return unless user - MergeRequests::RefreshService.new(project: project, current_user: user).execute(oldrev, newrev, ref) + push_options = params.with_indifferent_access[:push_options] + + MergeRequests::RefreshService + .new(project: project, current_user: user, params: { push_options: push_options }) + .execute(oldrev, newrev, ref) end end diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb index 88469d2cdef..5e6c1abdda6 100644 --- a/config/initializers/zz_metrics.rb +++ b/config/initializers/zz_metrics.rb @@ -32,7 +32,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware, Gitlab::Metrics::RackMiddleware - config.middleware.use(Gitlab::Middleware::RailsQueueDuration) + config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware, + Gitlab::Middleware::RailsQueueDuration + config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware) end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 11c232df4e0..fc75752061f 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -47,6 +47,8 @@ - 1 - - audit_events_user_impersonation_event_create - 1 +- - auth_saml_group_sync + - 1 - - authorized_keys - 2 - - authorized_project_update diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md index 5ccecfa9c8c..55f3cd7fd7a 100644 --- a/doc/administration/audit_event_streaming.md +++ b/doc/administration/audit_event_streaming.md @@ -158,9 +158,11 @@ the destination's value when [listing streaming destinations](#list-streaming-de > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.9 [with a flag](../administration/feature_flags.md) named `audit_event_streaming_git_operations`. Disabled by default. > - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/357211) in GitLab 15.0. +> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/357211) in GitLab 15.1 by default. FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](feature_flags.md) named `audit_event_streaming_git_operations`. On GitLab.com, this feature is available. +On self-managed GitLab, by default this feature is available. To hide the +feature, ask an administrator to [disable the feature flag](feature_flags.md) named `audit_event_streaming_git_operations`. Streaming audit events can be sent when signed-in users push or pull a project's remote Git repositories: diff --git a/doc/user/packages/infrastructure_registry/index.md b/doc/user/packages/infrastructure_registry/index.md index 47f563fd7e7..551289a575a 100644 --- a/doc/user/packages/infrastructure_registry/index.md +++ b/doc/user/packages/infrastructure_registry/index.md @@ -15,19 +15,13 @@ projects. ## View packages -To view packages within your project or group: +To view packages within your project: -1. Go to the project or group. +1. Go to the project. 1. Go to **Packages & Registries > Infrastructure Registry**. You can search, sort, and filter packages on this page. -When you view packages in a group: - -- All packages published to the group and its projects are displayed. -- Only the projects you can access are displayed. -- If a project is private, or you are not a member of the project, it is not displayed. - For information on how to create and upload a package, view the GitLab documentation for your package type: @@ -68,7 +62,7 @@ To delete a package, you must have suitable [permissions](../../permissions.md). You can delete packages by using [the API](../../../api/packages.md#delete-a-project-package) or the UI. -To delete a package in the UI, from your group or project: +To delete a package in the UI, from your project: 1. Go to **Packages & Registries > Infrastructure Registry**. 1. Find the name of the package you want to delete. diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb index 079a14824c0..0ba7e1b0fd0 100644 --- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb +++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb @@ -148,14 +148,6 @@ module Gitlab 'your migration class.' end - database_name = Gitlab::Database.db_config_name(connection) - - unless ActiveRecord::Base.configurations.primary?(database_name) - raise 'The `#finalize_background_migration` is currently not supported when running in decomposed database, ' \ - 'and this database is not `main:`. For more information visit: ' \ - 'https://docs.gitlab.com/ee/development/database/migrations_for_multiple_databases.html' - end - Gitlab::Database::BackgroundMigration::BatchedMigration.reset_column_information migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration( @@ -163,7 +155,14 @@ module Gitlab raise 'Could not find batched background migration' if migration.nil? - Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection) + with_restored_connection_stack do |restored_connection| + Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do + Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize( + job_class_name, table_name, + column_name, job_arguments, + connection: restored_connection) + end + end end # Deletes batched background migration for the given configuration. diff --git a/lib/gitlab/database/migrations/reestablished_connection_stack.rb b/lib/gitlab/database/migrations/reestablished_connection_stack.rb index d7cf482c32a..addc9d874af 100644 --- a/lib/gitlab/database/migrations/reestablished_connection_stack.rb +++ b/lib/gitlab/database/migrations/reestablished_connection_stack.rb @@ -17,7 +17,9 @@ module Gitlab original_handler = ActiveRecord::Base.connection_handler original_db_config = ActiveRecord::Base.connection_db_config - return yield if ActiveRecord::Base.configurations.primary?(original_db_config.name) + if ActiveRecord::Base.configurations.primary?(original_db_config.name) + return yield(ActiveRecord::Base.connection) + end # If the `ActiveRecord::Base` connection is different than `:main` # re-establish and configure `SharedModel` context accordingly @@ -43,7 +45,7 @@ module Gitlab ActiveRecord::Base.establish_connection :main # rubocop:disable Database/EstablishConnection Gitlab::Database::SharedModel.using_connection(base_model.connection) do - yield + yield(base_model.connection) end ensure ActiveRecord::Base.connection_handler = original_handler diff --git a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb index fe741a5bbe8..dde2bdd855e 100644 --- a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb +++ b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb @@ -6,21 +6,12 @@ module Gitlab module GrapeLogging module Loggers class QueueDurationLogger < ::GrapeLogging::Loggers::Base - attr_accessor :start_time - - def before - @start_time = Time.now - end - def parameters(request, _) - proxy_start = request.env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence + duration_s = request.env[Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY].presence - return {} unless proxy_start && start_time + return {} unless duration_s - # Time in milliseconds since gitlab-workhorse started the request - duration = start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000 - - { 'queue_duration_s': Gitlab::Utils.ms_to_round_sec(duration) } + { 'queue_duration_s': duration_s } end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 8a03c1e709b..a85a4b9b3e6 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -145,13 +145,18 @@ RSpec.describe Projects::IssuesController do project.add_developer(user) end - it "returns issue_email_participants" do + it "returns issue attributes" do participants = create_list(:issue_email_participant, 2, issue: issue) get :show, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }, format: :json expect(response).to have_gitlab_http_status(:ok) - expect(json_response['issue_email_participants']).to contain_exactly({ "email" => participants[0].email }, { "email" => participants[1].email }) + expect(json_response).to include( + 'issue_email_participants' => contain_exactly( + { "email" => participants[0].email }, { "email" => participants[1].email } + ), + 'type' => 'ISSUE' + ) end end diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json index 31743b58d98..b4a076780d9 100644 --- a/spec/fixtures/api/schemas/entities/issue.json +++ b/spec/fixtures/api/schemas/entities/issue.json @@ -3,6 +3,7 @@ "properties" : { "id": { "type": "integer" }, "iid": { "type": "integer" }, + "type": { "type": "string" }, "author_id": { "type": "integer" }, "description": { "type": ["string", "null"] }, "lock_version": { "type": ["integer", "null"] }, diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json index 58d3832440c..aa29ca08163 100644 --- a/spec/fixtures/api/schemas/entities/issue_board.json +++ b/spec/fixtures/api/schemas/entities/issue_board.json @@ -3,6 +3,7 @@ "properties" : { "id": { "type": "integer" }, "iid": { "type": "integer" }, + "type": { "type": "string" }, "title": { "type": "string" }, "confidential": { "type": "boolean" }, "closed": { "type": "boolean" }, diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 7de653e3069..4780851e9c1 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -8,8 +8,6 @@ import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; -import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql'; -import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; @@ -121,8 +119,6 @@ describe('CE IssuesListApp component', () => { const requestHandlers = [ [getIssuesQuery, issuesQueryResponse], [getIssuesCountsQuery, issuesCountsQueryResponse], - [getIssuesWithoutCrmQuery, issuesQueryResponse], - [getIssuesCountsWithoutCrmQuery, issuesCountsQueryResponse], [setSortPreferenceMutation, sortPreferenceMutationResponse], ]; diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js index e5f14cfc01a..603fb5cc2a6 100644 --- a/spec/frontend/issues/show/utils_spec.js +++ b/spec/frontend/issues/show/utils_spec.js @@ -2,7 +2,7 @@ import { convertDescriptionWithNewSort } from '~/issues/show/utils'; describe('app/assets/javascripts/issues/show/utils.js', () => { describe('convertDescriptionWithNewSort', () => { - it('converts markdown description with new list sort order', () => { + it('converts markdown description with nested lists with new list sort order', () => { const description = `I am text - Item 1 @@ -12,17 +12,17 @@ describe('app/assets/javascripts/issues/show/utils.js', () => { - Item 5`; // Drag Item 2 + children to Item 1's position - const html = `
    -
  • + const html = `
      +
    • Item 2 -
        -
      • Item 3
      • -
      • Item 4
      • +
          +
        • Item 3
        • +
        • Item 4
      • Item 1
      • -
      • Item 5
      • -
          `; +
        • Item 5
        • +
        `; const list = document.createElement('div'); list.innerHTML = html; @@ -36,5 +36,105 @@ describe('app/assets/javascripts/issues/show/utils.js', () => { expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected); }); + + it('converts markdown description with multi-line list items with new list sort order', () => { + const description = `Labore ea omnis et officia excepturi. + +1. Item 1 + + Item 1 part 2 + +1. Item 2 + - Item 2.1 + - Item 2.1.1 + - Item 2.1.2 + - Item 2.2 + - Item 2.3 +1. Item 3 +1. Item 4 + + \`\`\` + const variable = 'string'; + \`\`\` + + ![iii](img.jpg) + + last paragraph + +1. Item 5 +1. Item 6`; + + // Drag Item 2 + children to Item 5's position + const html = `
          +
        1. +

          Item 1

          +

          Item 1 part 2

          +
        2. +
        3. +

          Item 3

          +
        4. +
        5. +

          Item 4

          +
          +
          +              const variabl = 'string';
          +            
          +
          +

          + description +

          +

          last paragraph

          +
        6. +
        7. +

          Item 5

          +
        8. +
        9. +

          Item 2

          +
            +
          • Item 2.1 +
              +
            • Item 2.1.1
            • +
            • Item 2.1.2
            • +
            +
          • +
          • Item 2.2
          • +
          • Item 2.3
          • +
          +
        10. +
        11. +

          Item 6

          +
        12. +
        `; + const list = document.createElement('div'); + list.innerHTML = html; + + const expected = `Labore ea omnis et officia excepturi. + +1. Item 1 + + Item 1 part 2 + +1. Item 3 +1. Item 4 + + \`\`\` + const variable = 'string'; + \`\`\` + + ![iii](img.jpg) + + last paragraph + +1. Item 5 +1. Item 2 + - Item 2.1 + - Item 2.1.1 + - Item 2.1.2 + - Item 2.2 + - Item 2.3 +1. Item 6`; + + expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected); + }); }); }); diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js index 69f6a6e6e04..a286eeef14f 100644 --- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js @@ -1,5 +1,8 @@ import { shallowMount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; import { TEST_HOST } from 'helpers/test_constants'; +import { TYPE_USER } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue'; import userDataMock from '../../user_data_mock'; @@ -32,6 +35,7 @@ describe('AssigneeAvatarLink component', () => { }); const findTooltipText = () => wrapper.attributes('title'); + const findUserLink = () => wrapper.findComponent(GlLink); it('has the root url present in the assigneeUrl method', () => { createComponent(); @@ -112,4 +116,24 @@ describe('AssigneeAvatarLink component', () => { }); }, ); + + it('passes the correct user id for REST API', () => { + createComponent({ + tooltipHasName: true, + user: userDataMock(), + }); + + expect(findUserLink().attributes('data-user-id')).toBe(String(userDataMock().id)); + }); + + it('passes the correct user id for GraphQL API', () => { + const userId = userDataMock().id; + + createComponent({ + tooltipHasName: true, + user: { ...userDataMock(), id: convertToGraphQLId(TYPE_USER, userId) }, + }); + + expect(findUserLink().attributes('data-user-id')).toBe(String(userId)); + }); }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js new file mode 100644 index 00000000000..83fdc5d669d --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/index_spec.js @@ -0,0 +1,14 @@ +import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; +import { HLJS_ON_AFTER_HIGHLIGHT } from '~/vue_shared/components/source_viewer/constants'; +import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments'; + +jest.mock('~/vue_shared/components/source_viewer/plugins/wrap_comments'); +const hljsMock = { addPlugin: jest.fn() }; + +describe('Highlight.js plugin registration', () => { + beforeEach(() => registerPlugins(hljsMock)); + + it('registers our plugins', () => { + expect(hljsMock.addPlugin).toHaveBeenCalledWith({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js new file mode 100644 index 00000000000..5c0cee7781e --- /dev/null +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_comments_spec.js @@ -0,0 +1,20 @@ +import { HLJS_COMMENT_SELECTOR } from '~/vue_shared/components/source_viewer/constants'; +import wrapComments from '~/vue_shared/components/source_viewer/plugins/wrap_comments'; + +describe('Highlight.js plugin for wrapping comments', () => { + it('mutates the input value by wrapping each line in a span tag', () => { + const inputValue = `/* Line 1 \n* Line 2 \n*/`; + const outputValue = `/* Line 1 \n* Line 2 \n*/`; + const hljsResultMock = { value: inputValue }; + + wrapComments(hljsResultMock); + expect(hljsResultMock.value).toBe(outputValue); + }); + + it('does not mutate the input value if the hljs comment selector is not present', () => { + const inputValue = 'const'; + const hljsResultMock = { value: inputValue }; + + expect(hljsResultMock.value).toBe(inputValue); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js index 6a9ea75127d..bb0945a1f3e 100644 --- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; +import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; import { ROUGE_TO_HLJS_LANGUAGE_MAP } from '~/vue_shared/components/source_viewer/constants'; import waitForPromises from 'helpers/wait_for_promises'; @@ -11,6 +12,7 @@ import eventHub from '~/notes/event_hub'; jest.mock('~/blob/line_highlighter'); jest.mock('highlight.js/lib/core'); +jest.mock('~/vue_shared/components/source_viewer/plugins/index'); Vue.use(VueRouter); const router = new VueRouter(); @@ -59,6 +61,10 @@ describe('Source Viewer component', () => { describe('highlight.js', () => { beforeEach(() => createComponent({ language: mappedLanguage })); + it('registers our plugins for Highlight.js', () => { + expect(registerPlugins).toHaveBeenCalledWith(hljs); + }); + it('registers the language definition', async () => { const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`); diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb index b0caa21e01a..c423340a572 100644 --- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb @@ -444,7 +444,7 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do it 'does restore connection hierarchy' do expect_next_instances_of(job_class, 1..) do |job| expect(job).to receive(:perform) do - validate_connections! + validate_connections_stack! end end diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb index daddc510963..e1ddc83a3a4 100644 --- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb @@ -3,8 +3,14 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers do + let(:migration_class) do + Class.new(ActiveRecord::Migration[6.1]) + .include(described_class) + .include(Gitlab::Database::Migrations::ReestablishedConnectionStack) + end + let(:migration) do - ActiveRecord::Migration.new.extend(described_class) + migration_class.new end describe '#queue_batched_background_migration' do @@ -221,24 +227,64 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d end end - context 'when uses a CI connection', :reestablished_active_record_base do + context 'when within transaction' do before do - skip_if_multiple_databases_not_setup - - ActiveRecord::Base.establish_connection(:ci) # rubocop:disable Database/EstablishConnection + allow(migration).to receive(:transaction_open?).and_return(true) end - it 'raises an exception' do - ci_migration = create(:batched_background_migration, :active) + it 'does raise an exception' do + expect { migration.finalize_batched_background_migration(job_class_name: 'MyJobClass', table_name: :projects, column_name: :id, job_arguments: []) } + .to raise_error /`finalize_batched_background_migration` cannot be run inside a transaction./ + end + end - expect do - migration.finalize_batched_background_migration( - job_class_name: ci_migration.job_class_name, - table_name: ci_migration.table_name, - column_name: ci_migration.column_name, - job_arguments: ci_migration.job_arguments - ) - end.to raise_error /is currently not supported when running in decomposed/ + context 'when running migration in reconfigured ActiveRecord::Base context' do + it_behaves_like 'reconfigures connection stack', 'ci' do + before do + create(:batched_background_migration, + job_class_name: 'Ci::MyClass', + table_name: :ci_builds, + column_name: :id, + job_arguments: [], + gitlab_schema: :gitlab_ci) + end + + context 'when restrict_gitlab_migration is set to gitlab_ci' do + it 'finalizes the migration' do + migration_class.include(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema) + migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_ci + + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).to receive(:finalize).with('Ci::MyClass', :ci_builds, :id, []) do + validate_connections_stack! + end + end + + migration.finalize_batched_background_migration( + job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: []) + end + end + + context 'when restrict_gitlab_migration is set to gitlab_main' do + it 'does not find any migrations' do + migration_class.include(Gitlab::Database::MigrationHelpers::RestrictGitlabSchema) + migration_class.restrict_gitlab_migration gitlab_schema: :gitlab_main + + expect do + migration.finalize_batched_background_migration( + job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: []) + end.to raise_error /Could not find batched background migration/ + end + end + + context 'when no restrict is set' do + it 'does not find any migrations' do + expect do + migration.finalize_batched_background_migration( + job_class_name: 'Ci::MyClass', table_name: :ci_builds, column_name: :id, job_arguments: []) + end.to raise_error /Could not find batched background migration/ + end + end end end diff --git a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb index cfb308c63e4..d197f39be40 100644 --- a/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb +++ b/spec/lib/gitlab/database/migrations/reestablished_connection_stack_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Gitlab::Database::Migrations::ReestablishedConnectionStack do it_behaves_like "reconfigures connection stack", db_config_name do it 'does restore connection hierarchy' do model.with_restored_connection_stack do - validate_connections! + validate_connections_stack! end end diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb index 4cd9f9dfad0..1924cd687e4 100644 --- a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb +++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do describe ".parameters" do let(:start_time) { Time.new(2018, 01, 01) } - describe 'when no proxy time is available' do + describe 'when no proxy duration is available' do let(:mock_request) { double('env', env: {}) } it 'returns an empty hash' do @@ -16,20 +16,18 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do end end - describe 'when a proxy time is available' do + describe 'when a proxy duration is available' do let(:mock_request) do double('env', env: { - 'HTTP_GITLAB_WORKHORSE_PROXY_START' => (start_time - 1.hour).to_i * (10**9) + 'GITLAB_RAILS_QUEUE_DURATION' => 2.seconds } ) end - it 'returns the correct duration in seconds' do + it 'adds the duration to log parameters' do travel_to(start_time) do - subject.before - - expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 1.hour.to_f }) + expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration_s': 2.seconds.to_f }) end end end diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb index b8e2bfeaa3d..7a6a496912f 100644 --- a/spec/serializers/issue_board_entity_spec.rb +++ b/spec/serializers/issue_board_entity_spec.rb @@ -43,6 +43,12 @@ RSpec.describe IssueBoardEntity do expect(subject).to include(labels: array_including(hash_including(:id, :title, :color, :description, :text_color, :priority))) end + describe 'type' do + it 'has an issue type' do + expect(subject[:type]).to eq('ISSUE') + end + end + describe 'real_path' do it 'has an issue path' do expect(subject[:real_path]).to eq(project_issue_path(project, resource.iid)) diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb index 6ccb3dbc657..9525ed02314 100644 --- a/spec/serializers/issue_entity_spec.rb +++ b/spec/serializers/issue_entity_spec.rb @@ -24,6 +24,12 @@ RSpec.describe IssueEntity do end end + describe 'type' do + it 'has an issue type' do + expect(subject[:type]).to eq('ISSUE') + end + end + it 'has Issuable attributes' do expect(subject).to include(:id, :iid, :author_id, :description, :lock_version, :milestone_id, :title, :updated_by_id, :created_at, :updated_at, :milestone, :labels) diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb index d84ce8d15b4..08ad05b54da 100644 --- a/spec/services/merge_requests/create_pipeline_service_spec.rb +++ b/spec/services/merge_requests/create_pipeline_service_spec.rb @@ -50,6 +50,19 @@ RSpec.describe MergeRequests::CreatePipelineService do expect(response.payload.source).to eq('merge_request_event') end + context 'when push options contain ci.skip' do + let(:params) { { push_options: { ci: { skip: true } } } } + + it 'creates a skipped pipeline' do + expect { response }.to change { Ci::Pipeline.count }.by(1) + + expect(response).to be_success + expect(response.payload).to be_persisted + expect(response.payload.builds).to be_empty + expect(response.payload).to be_skipped + end + end + context 'with fork merge request' do let_it_be(:forked_project) { fork_project(project, nil, repository: true, target_project: create(:project, :private, :repository)) } diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 6e6b4a91e0d..7560f0d5e24 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -228,6 +228,21 @@ RSpec.describe MergeRequests::RefreshService do expect(@another_merge_request.has_commits?).to be_falsy end + context 'when "push_options: nil" is passed' do + let(:service_instance) { service.new(project: project, current_user: @user, params: { push_options: nil }) } + + subject { service_instance.execute(@oldrev, @newrev, ref) } + + it 'creates a detached merge request pipeline with commits' do + expect { subject } + .to change { @merge_request.pipelines_for_merge_request.count }.by(1) + .and change { @another_merge_request.pipelines_for_merge_request.count }.by(0) + + expect(@merge_request.has_commits?).to be_truthy + expect(@another_merge_request.has_commits?).to be_falsy + end + end + it 'does not create detached merge request pipeline for forked project' do expect { subject } .not_to change { @fork_merge_request.pipelines_for_merge_request.count } diff --git a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb index 67d739b79ab..d14216ec5ff 100644 --- a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb @@ -22,7 +22,7 @@ RSpec.shared_context 'reconfigures connection stack' do |db_config_name| end end - def validate_connections! + def validate_connections_stack! model_connections = Gitlab::Database.database_base_models.to_h do |db_config_name, model_class| [model_class, Gitlab::Database.db_config_name(model_class.connection)] end diff --git a/spec/workers/merge_requests/create_pipeline_worker_spec.rb b/spec/workers/merge_requests/create_pipeline_worker_spec.rb index 06d44c45706..441d7652219 100644 --- a/spec/workers/merge_requests/create_pipeline_worker_spec.rb +++ b/spec/workers/merge_requests/create_pipeline_worker_spec.rb @@ -3,24 +3,50 @@ require 'spec_helper' RSpec.describe MergeRequests::CreatePipelineWorker do - subject(:worker) { described_class.new } - describe '#perform' do let(:user) { create(:user) } let(:project) { create(:project) } let(:merge_request) { create(:merge_request) } + let(:worker) { described_class.new } + + subject { worker.perform(project.id, user.id, merge_request.id) } context 'when the objects exist' do it 'calls the merge request create pipeline service and calls update head pipeline' do aggregate_failures do - expect_next_instance_of(MergeRequests::CreatePipelineService, project: project, current_user: user) do |service| + expect_next_instance_of(MergeRequests::CreatePipelineService, + project: project, + current_user: user, + params: { push_options: nil }) do |service| expect(service).to receive(:execute).with(merge_request) end expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request) expect(merge_request).to receive(:update_head_pipeline) - subject.perform(project.id, user.id, merge_request.id) + subject + end + end + + context 'when push options are passed as Hash to the worker' do + let(:extra_params) { { 'push_options' => { 'ci' => { 'skip' => true } } } } + + subject { worker.perform(project.id, user.id, merge_request.id, extra_params) } + + it 'calls the merge request create pipeline service and calls update head pipeline' do + aggregate_failures do + expect_next_instance_of(MergeRequests::CreatePipelineService, + project: project, + current_user: user, + params: { push_options: { ci: { skip: true } } }) do |service| + expect(service).to receive(:execute).with(merge_request) + end + + expect(MergeRequest).to receive(:find_by_id).with(merge_request.id).and_return(merge_request) + expect(merge_request).to receive(:update_head_pipeline) + + subject + end end end end @@ -29,8 +55,7 @@ RSpec.describe MergeRequests::CreatePipelineWorker do it 'does not call the create pipeline service' do expect(MergeRequests::CreatePipelineService).not_to receive(:new) - expect { subject.perform(project.id, user.id, merge_request.id) } - .not_to raise_exception + expect { subject }.not_to raise_exception end end diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb index bd0dc2f9ef4..64fcc2bd388 100644 --- a/spec/workers/update_merge_requests_worker_spec.rb +++ b/spec/workers/update_merge_requests_worker_spec.rb @@ -3,28 +3,47 @@ require 'spec_helper' RSpec.describe UpdateMergeRequestsWorker do - include RepoHelpers + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:oldrev) { "123456" } + let_it_be(:newrev) { "789012" } + let_it_be(:ref) { "refs/heads/test" } - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - - subject { described_class.new } + let(:worker) { described_class.new } describe '#perform' do - let(:oldrev) { "123456" } - let(:newrev) { "789012" } - let(:ref) { "refs/heads/test" } - - def perform - subject.perform(project.id, user.id, oldrev, newrev, ref) - end + subject { worker.perform(project.id, user.id, oldrev, newrev, ref) } it 'executes MergeRequests::RefreshService with expected values' do - expect_next_instance_of(MergeRequests::RefreshService, project: project, current_user: user) do |refresh_service| - expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref) + expect_next_instance_of(MergeRequests::RefreshService, + project: project, + current_user: user, + params: { push_options: nil }) do |service| + expect(service) + .to receive(:execute) + .with(oldrev, newrev, ref) end - perform + subject + end + + context 'when push options are passed as Hash' do + let(:extra_params) { { 'push_options' => { 'ci' => { 'skip' => true } } } } + + subject { worker.perform(project.id, user.id, oldrev, newrev, ref, extra_params) } + + it 'executes MergeRequests::RefreshService with expected values' do + expect_next_instance_of(MergeRequests::RefreshService, + project: project, + current_user: user, + params: { push_options: { ci: { skip: true } } }) do |service| + expect(service) + .to receive(:execute) + .with(oldrev, newrev, ref) + end + + subject + end end end end