diff options
100 files changed, 2207 insertions, 684 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6609868689d..cafba1c6149 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -199,21 +199,6 @@ RSpec/ExpectChange: RSpec/ExpectInHook: Enabled: false -# Offense count: 68 -# Cop supports --auto-correct. -RSpec/LetBeforeExamples: - Exclude: - - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb' - - 'spec/lib/banzai/filter/user_reference_filter_spec.rb' - - 'spec/lib/gitlab/email/handler/create_issue_handler_spec.rb' - - 'spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb' - - 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb' - - 'spec/models/commit_range_spec.rb' - - 'spec/models/milestone_spec.rb' - - 'spec/models/project_services/packagist_service_spec.rb' - - 'spec/rubocop/cop/migration/update_column_in_batches_spec.rb' - - 'spec/serializers/pipeline_details_entity_spec.rb' - # Offense count: 2188 # Cop supports --auto-correct. # Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. @@ -679,13 +664,6 @@ Style/RescueModifier: Style/RescueStandardError: Enabled: false -# Offense count: 50 -# Cop supports --auto-correct. -# Configuration parameters: AllowIfMethodIsEmpty. -Style/SingleLineMethods: - Exclude: - - 'lib/gitlab/ci/ansi2html.rb' - # Offense count: 102 # Cop supports --auto-correct. # Configuration parameters: . diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e7ceb275bc4..edc9f9d47b2 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -a783958be5ae0797dac9041bdfb884440e0e6306 +02ae27efafdf367d991eac43df02b892be378a1b diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index e6ea441c95a..15a22d1767c 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -1,8 +1,7 @@ <script> /* eslint-disable vue/no-v-html */ import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; -import DiffTableCell from './diff_table_cell.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { MATCH_LINE_TYPE, NEW_LINE_TYPE, @@ -11,11 +10,19 @@ import { CONTEXT_LINE_CLASS_NAME, LINE_POSITION_LEFT, LINE_POSITION_RIGHT, + LINE_HOVER_CLASS_NAME, + OLD_NO_NEW_LINE_TYPE, + NEW_NO_NEW_LINE_TYPE, + EMPTY_CELL_TYPE, } from '../constants'; +import { __ } from '~/locale'; +import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; +import DiffGutterAvatars from './diff_gutter_avatars.vue'; export default { components: { - DiffTableCell, + DiffGutterAvatars, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -50,6 +57,7 @@ export default { }; }, computed: { + ...mapGetters(['isLoggedIn']), ...mapGetters('diffs', ['fileLineCoverage']), ...mapState({ isHighlighted(state) { @@ -79,6 +87,70 @@ export default { coverageState() { return this.fileLineCoverage(this.filePath, this.line.new_line); }, + isMetaLine() { + const { type } = this.line; + + return ( + type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE + ); + }, + classNameMapCell() { + const { type } = this.line; + + return [ + type, + { + hll: this.isHighlighted, + [LINE_HOVER_CLASS_NAME]: + this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine, + }, + ]; + }, + addCommentTooltip() { + const brokenSymlinks = this.line.commentsDisabled; + let tooltip = __('Add a comment to this line'); + + if (brokenSymlinks) { + if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) { + tooltip = __( + 'Commenting on symbolic links that replace or are replaced by files is currently not supported.', + ); + } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) { + tooltip = __( + 'Commenting on files that replace or are replaced by symbolic links is currently not supported.', + ); + } + } + + return tooltip; + }, + shouldRenderCommentButton() { + if (this.isLoggedIn) { + const isDiffHead = parseBoolean(getParameterByName('diff_head')); + return !isDiffHead || gon.features?.mergeRefHeadComments; + } + + return false; + }, + shouldShowCommentButton() { + return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions; + }, + hasDiscussions() { + return this.line.discussions && this.line.discussions.length > 0; + }, + lineHref() { + return `#${this.line.line_code || ''}`; + }, + lineCode() { + return ( + this.line.line_code || + (this.line.left && this.line.left.line_code) || + (this.line.right && this.line.right.line_code) + ); + }, + shouldShowAvatarsOnGutter() { + return this.hasDiscussions; + }, }, created() { this.newLineType = NEW_LINE_TYPE; @@ -90,12 +162,20 @@ export default { this.scrollToLineIfNeededInline(this.line); }, methods: { - ...mapActions('diffs', ['scrollToLineIfNeededInline']), + ...mapActions('diffs', [ + 'scrollToLineIfNeededInline', + 'showCommentForm', + 'setHighlightedRow', + 'toggleLineDiscussions', + ]), handleMouseMove(e) { // To show the comment icon on the gutter we need to know if we hover the line. // Current table structure doesn't allow us to do this with CSS in both of the diff view types this.isHover = e.type === 'mouseover'; }, + handleCommentButton() { + this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); + }, }, }; </script> @@ -109,25 +189,52 @@ export default { @mouseover="handleMouseMove" @mouseout="handleMouseMove" > - <diff-table-cell - :file-hash="fileHash" - :line="line" - :line-type="oldLineType" - :is-bottom="isBottom" - :is-hover="isHover" - :show-comment-button="true" - :is-highlighted="isHighlighted" - class="diff-line-num old_line" - /> - <diff-table-cell - :file-hash="fileHash" - :line="line" - :line-type="newLineType" - :is-bottom="isBottom" - :is-hover="isHover" - :is-highlighted="isHighlighted" - class="diff-line-num new_line qa-new-diff-line" - /> + <td ref="oldTd" class="diff-line-num old_line" :class="classNameMapCell"> + <span + v-if="shouldRenderCommentButton" + ref="addNoteTooltip" + v-gl-tooltip + class="add-diff-note tooltip-wrapper" + :title="addCommentTooltip" + > + <button + v-show="shouldShowCommentButton" + ref="addDiffNoteButton" + type="button" + class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :disabled="line.commentsDisabled" + @click="handleCommentButton" + > + <gl-icon :size="12" name="comment" /> + </button> + </span> + <a + v-if="line.old_line" + ref="lineNumberRefOld" + :data-linenumber="line.old_line" + :href="lineHref" + @click="setHighlightedRow(lineCode)" + > + </a> + <diff-gutter-avatars + v-if="shouldShowAvatarsOnGutter" + :discussions="line.discussions" + :discussions-expanded="line.discussionsExpanded" + @toggleLineDiscussions=" + toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded }) + " + /> + </td> + <td ref="newTd" class="diff-line-num new_line qa-new-diff-line" :class="classNameMapCell"> + <a + v-if="line.new_line" + ref="lineNumberRefNew" + :data-linenumber="line.new_line" + :href="lineHref" + @click="setHighlightedRow(lineCode)" + > + </a> + </td> <td v-gl-tooltip.hover :title="coverageState.text" diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 152819b4cb5..a13a0dbbf30 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -1,7 +1,7 @@ <script> /* eslint-disable vue/no-v-html */ import { mapActions } from 'vuex'; -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { @@ -10,6 +10,7 @@ export default { GitlabTeamMemberBadge: () => import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'), GlIcon, + GlLoadingIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -195,13 +196,12 @@ export default { class="gl-ml-1 gl-text-gray-700 align-middle" /> <slot name="extra-controls"></slot> - <i + <gl-loading-icon v-if="showSpinner" ref="spinner" - class="fa fa-spinner fa-spin editing-spinner" - :aria-label="__('Comment is being updated')" - aria-hidden="true" - ></i> + class="editing-spinner" + :label="__('Comment is being updated')" + /> </span> </div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 7116a007ebe..e5f1fbff687 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -1,9 +1,8 @@ <script> -/* eslint-disable vue/no-v-html */ import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import { escape } from 'lodash'; -import { GlSprintf } from '@gitlab/ui'; +import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; @@ -35,6 +34,9 @@ export default { NoteBody, TimelineEntryItem, }, + directives: { + SafeHtml, + }, mixins: [noteable, resolvable, glFeatureFlagsMixin()], props: { note: { @@ -379,7 +381,7 @@ export default { :is-confidential="note.confidential" > <slot slot="note-header-info" name="note-header-info"></slot> - <span v-if="commit" v-html="actionText"></span> + <span v-if="commit" v-safe-html="actionText"></span> <span v-else-if="note.created_at" class="d-none d-sm-inline">·</span> </note-header> <note-actions diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue index dbb5f7be0a0..56591fb6b15 100644 --- a/app/assets/javascripts/packages/details/components/app.vue +++ b/app/assets/javascripts/packages/details/components/app.vue @@ -268,22 +268,24 @@ export default { </template> </gl-sprintf> - <div slot="modal-footer" class="w-100"> - <div class="float-right"> - <gl-button @click="cancelDelete()">{{ __('Cancel') }}</gl-button> - <gl-button - ref="modal-delete-button" - data-method="delete" - :to="destroyPath" - variant="danger" - category="primary" - data-qa-selector="delete_modal_button" - @click="track($options.trackingActions.DELETE_PACKAGE)" - > - {{ __('Delete') }} - </gl-button> + <template #modal-footer> + <div class="gl-w-full"> + <div class="float-right"> + <gl-button @click="cancelDelete">{{ __('Cancel') }}</gl-button> + <gl-button + ref="modal-delete-button" + data-method="delete" + :to="destroyPath" + variant="danger" + category="primary" + data-qa-selector="delete_modal_button" + @click="track($options.trackingActions.DELETE_PACKAGE)" + > + {{ __('Delete') }} + </gl-button> + </div> </div> - </div> + </template> </gl-modal> </div> </template> diff --git a/app/assets/javascripts/packages/details/components/code_instruction.vue b/app/assets/javascripts/packages/details/components/code_instruction.vue index 0719ddfcd2b..5200857a9b9 100644 --- a/app/assets/javascripts/packages/details/components/code_instruction.vue +++ b/app/assets/javascripts/packages/details/components/code_instruction.vue @@ -48,7 +48,7 @@ export default { <input :value="instruction" type="text" - class="form-control monospace js-instruction-input" + class="form-control gl-font-monospace js-instruction-input" readonly @copy="trackCopy" /> diff --git a/app/assets/javascripts/packages/details/components/dependency_row.vue b/app/assets/javascripts/packages/details/components/dependency_row.vue index 788673d2881..367ed6ca22a 100644 --- a/app/assets/javascripts/packages/details/components/dependency_row.vue +++ b/app/assets/javascripts/packages/details/components/dependency_row.vue @@ -26,7 +26,7 @@ export default { <div v-if="showVersion" - class="table-section section-50 gl-display-flex justify-content-md-end" + class="table-section section-50 gl-display-flex gl-justify-content-md-end" data-testid="version-pattern" > <span class="gl-text-body">{{ dependency.version_pattern }}</span> diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue index b26c6087e14..19d0a914d41 100644 --- a/app/assets/javascripts/packages/list/components/packages_list.vue +++ b/app/assets/javascripts/packages/list/components/packages_list.vue @@ -82,7 +82,7 @@ export default { </script> <template> - <div class="d-flex flex-column"> + <div class="gl-display-flex gl-flex-direction-column"> <slot v-if="isListEmpty && !isLoading" name="empty-state"></slot> <div v-else-if="isLoading"> @@ -106,7 +106,7 @@ export default { :per-page="perPage" :total-items="totalItems" align="center" - class="w-100 mt-2" + class="gl-w-full gl-mt-3" /> <gl-modal diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue index ef242ea5f75..9c7725477eb 100644 --- a/app/assets/javascripts/packages/list/components/packages_list_app.vue +++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue @@ -77,7 +77,9 @@ export default { <template> <gl-tabs @input="tabChanged"> <template #tabs-end> - <div class="d-flex align-self-center ml-md-auto py-1 py-md-0"> + <div + class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end" + > <package-filter class="mr-1" @filter="requestPackagesList" /> <package-sort @sort:changed="requestPackagesList" /> </div> diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue index f51ca26abf2..3d7e233c1ba 100644 --- a/app/assets/javascripts/packages/shared/components/package_tags.vue +++ b/app/assets/javascripts/packages/shared/components/package_tags.vue @@ -91,7 +91,7 @@ export default { variant="muted" :title="moreTagsTooltip" size="sm" - class="gl-display-none d-md-flex gl-ml-2" + class="gl-display-none gl-display-md-flex gl-ml-2" ><gl-sprintf :message="__('+%{tags} more')"> <template #tags> {{ moreTagsDisplay }} @@ -103,7 +103,7 @@ export default { v-if="moreTagsDisplay && hideLabel" data-testid="moreBadge" variant="muted" - class="d-md-none gl-ml-2" + class="gl-display-md-none gl-ml-2" >{{ tagsDisplay }}</gl-badge > </div> diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue index cd9ef74d467..62038f13dd9 100644 --- a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue +++ b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue @@ -48,7 +48,7 @@ export default { <template> <div> - <div class="d-xs-flex flex-column d-md-none"> + <div class="gl-display-sm-flex gl-flex-direction-column gl-display-md-none"> <gl-skeleton-loader v-for="index in $options.rowsToRender.mobile" :key="index" @@ -65,7 +65,7 @@ export default { </gl-skeleton-loader> </div> - <div class="d-none d-md-flex flex-column"> + <div class="gl-display-none gl-display-md-flex gl-flex-direction-column"> <gl-skeleton-loader v-for="index in $options.rowsToRender.desktop" :key="index" diff --git a/app/assets/javascripts/packages/shared/components/publish_method.vue b/app/assets/javascripts/packages/shared/components/publish_method.vue index 84e89d3b2d4..468cc458ed9 100644 --- a/app/assets/javascripts/packages/shared/components/publish_method.vue +++ b/app/assets/javascripts/packages/shared/components/publish_method.vue @@ -36,24 +36,28 @@ export default { </script> <template> - <div class="d-flex align-items-center order-1 order-md-0 mb-md-1"> + <div class="gl-display-flex gl-align-items-center gl-mb-2"> <template v-if="hasPipeline"> - <gl-icon name="git-merge" class="mr-1" /> - <span ref="pipeline-ref" class="mr-1">{{ packageEntity.pipeline.ref }}</span> + <gl-icon name="git-merge" class="gl-mr-2" /> + <span data-testid="pipeline-ref" class="gl-mr-2">{{ packageEntity.pipeline.ref }}</span> - <gl-icon name="commit" class="mr-1" /> - <gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link> + <gl-icon name="commit" class="gl-mr-2" /> + <gl-link data-testid="pipeline-sha" :href="linkToCommit" class="gl-mr-2">{{ + packageShaShort + }}</gl-link> <clipboard-button :text="packageEntity.pipeline.sha" :title="__('Copy commit SHA')" - css-class="border-0 py-0 px-1" + css-class="gl-border-0 gl-py-0 gl-px-2" /> </template> <template v-else> - <gl-icon name="upload" class="mr-1" /> - <span ref="manual-ref">{{ s__('PackageRegistry|Manually Published') }}</span> + <gl-icon name="upload" class="gl-mr-2" /> + <span data-testid="manually-published"> + {{ s__('PackageRegistry|Manually Published') }} + </span> </template> </div> </template> diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js index 047964260ad..9fa2c589324 100644 --- a/app/assets/javascripts/reports/accessibility_report/store/index.js +++ b/app/assets/javascripts/reports/accessibility_report/store/index.js @@ -7,10 +7,11 @@ import state from './state'; Vue.use(Vuex); -export default initialState => - new Vuex.Store({ - actions, - getters, - mutations, - state: state(initialState), - }); +export const getStoreConfig = initialState => ({ + actions, + getters, + mutations, + state: state(initialState), +}); + +export default initialState => new Vuex.Store(getStoreConfig(initialState)); diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js index 047964260ad..9fa2c589324 100644 --- a/app/assets/javascripts/reports/codequality_report/store/index.js +++ b/app/assets/javascripts/reports/codequality_report/store/index.js @@ -7,10 +7,11 @@ import state from './state'; Vue.use(Vuex); -export default initialState => - new Vuex.Store({ - actions, - getters, - mutations, - state: state(initialState), - }); +export const getStoreConfig = initialState => ({ + actions, + getters, + mutations, + state: state(initialState), +}); + +export default initialState => new Vuex.Store(getStoreConfig(initialState)); diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js index 467c692b438..a2edfa94a48 100644 --- a/app/assets/javascripts/reports/store/index.js +++ b/app/assets/javascripts/reports/store/index.js @@ -7,10 +7,11 @@ import state from './state'; Vue.use(Vuex); -export default () => - new Vuex.Store({ - actions, - mutations, - getters, - state: state(), - }); +export const getStoreConfig = () => ({ + actions, + mutations, + getters, + state: state(), +}); + +export default () => new Vuex.Store(getStoreConfig()); diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index 6818c8df6db..b27602f6b4a 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -40,7 +40,6 @@ @import './pages/note_form'; @import './pages/notes'; @import './pages/notifications'; -@import './pages/packages'; @import './pages/pages'; @import './pages/pipeline_schedules'; @import './pages/pipelines'; diff --git a/app/assets/stylesheets/pages/packages.scss b/app/assets/stylesheets/pages/packages.scss deleted file mode 100644 index 8f6eee524e5..00000000000 --- a/app/assets/stylesheets/pages/packages.scss +++ /dev/null @@ -1,11 +0,0 @@ -.commit-row-description { - border: 0; - border-left: 3px solid $white-dark; -} - -.package-list-table[aria-busy='true'] { - td { - padding-bottom: 0; - padding-top: 0; - } -} diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 20482f30994..6792a27f2cd 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -160,3 +160,20 @@ min-height: $gl-spacing-scale-6; } +.gl-justify-content-md-end { + @media (min-width: $breakpoint-md) { + width: auto !important; + } +} + +.gl-display-md-flex { + @media (min-width: $breakpoint-md) { + display: flex; + } +} + +.gl-display-md-none { + @media (min-width: $breakpoint-md) { + display: none; + } +} diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index d969e7bf771..cdcc069e1c4 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -10,6 +10,8 @@ class Projects::BlobController < Projects::ApplicationController include RedirectsForMissingPathOnTree include SourcegraphDecorator include DiffHelper + include RedisTracking + extend ::Gitlab::Utils::Override prepend_before_action :authenticate_user!, only: [:edit] @@ -35,6 +37,8 @@ class Projects::BlobController < Projects::ApplicationController push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline) end + track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions + def new commit unless @repository.empty? end @@ -256,4 +260,9 @@ class Projects::BlobController < Projects::ApplicationController def diff_params params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent) end + + override :visitor_id + def visitor_id + current_user&.id + end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 02e800eb86f..5f9a7fda02a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -37,6 +37,7 @@ class IssuableFinder include FinderMethods include CreatedAtFilter include Gitlab::Utils::StrongMemoize + prepend OptimizedIssuableLabelFilter requires_cross_project_access unless: -> { params.project? } diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index e726772fba4..4358cf249f7 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -172,7 +172,14 @@ class LabelsFinder < UnionFinder ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute # rubocop: disable CodeReuse/Finder end - @projects = @projects.in_namespace(group.id) if group? + if group? + @projects = if params[:include_subgroups] + @projects.in_namespace(group.self_and_descendants.select(:id)) + else + @projects.in_namespace(group.id) + end + end + @projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.reorder(nil) diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb index a7cc367379d..dba9f99edeb 100644 --- a/app/graphql/resolvers/board_list_issues_resolver.rb +++ b/app/graphql/resolvers/board_list_issues_resolver.rb @@ -2,12 +2,20 @@ module Resolvers class BoardListIssuesResolver < BaseResolver + include BoardIssueFilterable + + argument :filters, Types::Boards::BoardIssueInputType, + required: false, + description: 'Filters applied when selecting issues in the board list' + type Types::IssueType, null: true alias_method :list, :object def resolve(**args) - service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], { board_id: list.board.id, id: list.id }) + filter_params = issue_filters(args[:filters]).merge(board_id: list.board.id, id: list.id) + service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params) + Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute) end diff --git a/app/graphql/resolvers/concerns/board_issue_filterable.rb b/app/graphql/resolvers/concerns/board_issue_filterable.rb new file mode 100644 index 00000000000..1541738f46c --- /dev/null +++ b/app/graphql/resolvers/concerns/board_issue_filterable.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module BoardIssueFilterable + extend ActiveSupport::Concern + + private + + def issue_filters(args) + filters = args.to_h + set_filter_values(filters) + + if filters[:not] + filters[:not] = filters[:not].to_h + set_filter_values(filters[:not]) + end + + filters + end + + def set_filter_values(filters) + end +end + +::BoardIssueFilterable.prepend_if_ee('::EE::Resolvers::BoardIssueFilterable') diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb index 70c0794fc90..24faf1fe8bc 100644 --- a/app/graphql/types/board_list_type.rb +++ b/app/graphql/types/board_list_type.rb @@ -41,7 +41,7 @@ module Types list = self.object user = context[:current_user] - Boards::Issues::ListService + ::Boards::Issues::ListService .new(list.board.resource_parent, user, board_id: list.board_id, id: list.id) .metadata end diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb new file mode 100644 index 00000000000..1187b3352cd --- /dev/null +++ b/app/graphql/types/boards/board_issue_input_base_type.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Types + module Boards + # rubocop: disable Graphql/AuthorizeTypes + class BoardIssueInputBaseType < BaseInputObject + argument :label_name, GraphQL::STRING_TYPE.to_list_type, + required: false, + description: 'Filter by label name' + + argument :milestone_title, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by milestone title' + + argument :assignee_username, GraphQL::STRING_TYPE.to_list_type, + required: false, + description: 'Filter by assignee username' + + argument :author_username, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by author username' + + argument :release_tag, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by release tag' + + argument :my_reaction_emoji, GraphQL::STRING_TYPE, + required: false, + description: 'Filter by reaction emoji' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end + +Types::Boards::BoardIssueInputBaseType.prepend_if_ee('::EE::Types::Boards::BoardIssueInputBaseType') diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb new file mode 100644 index 00000000000..40d065d8ea9 --- /dev/null +++ b/app/graphql/types/boards/board_issue_input_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + module Boards + # rubocop: disable Graphql/AuthorizeTypes + class NegatedBoardIssueInputType < BoardIssueInputBaseType + end + + class BoardIssueInputType < BoardIssueInputBaseType + graphql_name 'BoardIssueInput' + + argument :not, NegatedBoardIssueInputType, + required: false, + description: 'List of negated params. Warning: this argument is experimental and a subject to change in future' + + argument :search, GraphQL::STRING_TYPE, + required: false, + description: 'Search query for issue title or description' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end + +Types::Boards::BoardIssueInputType.prepend_if_ee('::EE::Types::Boards::BoardIssueInputType') diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb new file mode 100644 index 00000000000..7be4a26d4fa --- /dev/null +++ b/app/models/concerns/optimized_issuable_label_filter.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +module OptimizedIssuableLabelFilter + def by_label(items) + return items unless params.labels? + + return super if Feature.disabled?(:optimized_issuable_label_filter) + + target_model = items.model + + if params.filter_by_no_label? + items.where('NOT EXISTS (?)', optimized_any_label_query(target_model)) + elsif params.filter_by_any_label? + items.where('EXISTS (?)', optimized_any_label_query(target_model)) + else + issuables_with_selected_labels(items, target_model) + end + end + + # Taken from IssuableFinder + def count_by_state + return super if root_namespace.nil? + return super if Feature.disabled?(:optimized_issuable_label_filter) + + count_params = params.merge(state: nil, sort: nil, force_cte: true) + finder = self.class.new(current_user, count_params) + + state_counts = finder + .execute + .reorder(nil) + .group(:state_id) + .count + + counts = state_counts.transform_keys { |key| count_key(key) } + + counts[:all] = counts.values.sum + counts.with_indifferent_access + end + + private + + def issuables_with_selected_labels(items, target_model) + if root_namespace + all_label_ids = find_label_ids(root_namespace) + # Found less labels in the DB than we were searching for. Return nothing. + return items.none if all_label_ids.size != params.label_names.size + + all_label_ids.each do |label_ids| + items = items.where('EXISTS (?)', optimized_label_query_by_label_ids(target_model, label_ids)) + end + else + params.label_names.each do |label_name| + items = items.where('EXISTS (?)', optimized_label_query_by_label_name(target_model, label_name)) + end + end + + items + end + + def find_label_ids(root_namespace) + finder_params = { + include_subgroups: true, + include_ancestor_groups: true, + include_descendant_groups: true, + group: root_namespace, + title: params.label_names + } + + LabelsFinder + .new(nil, finder_params) + .execute(skip_authorization: true) + .pluck(:title, :id) + .group_by(&:first) + .values + .map { |labels| labels.map(&:last) } + end + + def root_namespace + strong_memoize(:root_namespace) do + (params.project || params.group)&.root_ancestor + end + end + + def optimized_any_label_query(target_model) + LabelLink + .where(target_type: target_model.name) + .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id'])) + .limit(1) + end + + def optimized_label_query_by_label_ids(target_model, label_ids) + LabelLink + .where(target_type: target_model.name) + .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id'])) + .where(label_id: label_ids) + .limit(1) + end + + def optimized_label_query_by_label_name(target_model, label_name) + LabelLink + .joins(:label) + .where(target_type: target_model.name) + .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id'])) + .where(labels: { name: label_name }) + .limit(1) + end +end diff --git a/app/services/ci/create_cross_project_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb index 23207d809d4..23cd06fbd4f 100644 --- a/app/services/ci/create_cross_project_pipeline_service.rb +++ b/app/services/ci/create_downstream_pipeline_service.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true module Ci - # TODO: rename this (and worker) to CreateDownstreamPipelineService - class CreateCrossProjectPipelineService < ::BaseService + # Takes in input a Ci::Bridge job and creates a downstream pipeline + # (either multi-project or child pipeline) according to the Ci::Bridge + # specifications. + class CreateDownstreamPipelineService < ::BaseService include Gitlab::Utils::StrongMemoize DuplicateDownstreamPipelineError = Class.new(StandardError) diff --git a/app/services/packages/composer/create_package_service.rb b/app/services/packages/composer/create_package_service.rb index ad5d267698b..7e16fc78599 100644 --- a/app/services/packages/composer/create_package_service.rb +++ b/app/services/packages/composer/create_package_service.rb @@ -2,7 +2,7 @@ module Packages module Composer - class CreatePackageService < BaseService + class CreatePackageService < ::Packages::CreatePackageService include ::Gitlab::Utils::StrongMemoize def execute @@ -21,10 +21,7 @@ module Packages private def created_package - project - .packages - .composer - .safe_find_or_create_by!(name: package_name, version: package_version) + find_or_create_package!(:composer, name: package_name, version: package_version) end def composer_json diff --git a/app/services/packages/conan/create_package_service.rb b/app/services/packages/conan/create_package_service.rb index 22a0436c5fb..35046d8776e 100644 --- a/app/services/packages/conan/create_package_service.rb +++ b/app/services/packages/conan/create_package_service.rb @@ -2,12 +2,11 @@ module Packages module Conan - class CreatePackageService < BaseService + class CreatePackageService < ::Packages::CreatePackageService def execute - project.packages.create!( + create_package!(:conan, name: params[:package_name], version: params[:package_version], - package_type: :conan, conan_metadatum_attributes: { package_username: params[:package_username], package_channel: params[:package_channel] diff --git a/app/services/packages/create_package_service.rb b/app/services/packages/create_package_service.rb new file mode 100644 index 00000000000..05b0b8e9fa6 --- /dev/null +++ b/app/services/packages/create_package_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Packages + class CreatePackageService < BaseService + protected + + def find_or_create_package!(package_type, name: params[:name], version: params[:version]) + project + .packages + .with_package_type(package_type) + .safe_find_or_create_by!(name: name, version: version) + end + + def create_package!(package_type, attrs = {}) + project + .packages + .with_package_type(package_type) + .create!(package_attrs(attrs)) + end + + private + + def package_attrs(attrs) + { + name: params[:name], + version: params[:version] + }.merge(attrs) + end + end +end diff --git a/app/services/packages/maven/create_package_service.rb b/app/services/packages/maven/create_package_service.rb index aca5d28ca98..3df17021499 100644 --- a/app/services/packages/maven/create_package_service.rb +++ b/app/services/packages/maven/create_package_service.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true module Packages module Maven - class CreatePackageService < BaseService + class CreatePackageService < ::Packages::CreatePackageService def execute app_group, _, app_name = params[:name].rpartition('/') app_group.tr!('/', '.') - package = project.packages.create!( - name: params[:name], - version: params[:version], - package_type: :maven, + package = create_package!(:maven, maven_metadatum_attributes: { path: params[:path], app_group: app_group, diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb index 7eac45c00ca..7f868b71734 100644 --- a/app/services/packages/npm/create_package_service.rb +++ b/app/services/packages/npm/create_package_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Packages module Npm - class CreatePackageService < BaseService + class CreatePackageService < ::Packages::CreatePackageService include Gitlab::Utils::StrongMemoize def execute @@ -9,17 +9,13 @@ module Packages return error('Package already exists.', 403) if current_package_exists? return error('File is too large.', 400) if file_size_exceeded? - ActiveRecord::Base.transaction { create_package! } + ActiveRecord::Base.transaction { create_npm_package! } end private - def create_package! - package = project.packages.create!( - name: name, - version: version, - package_type: 'npm' - ) + def create_npm_package! + package = create_package!(:npm, name: name, version: version) if build.present? package.create_build_info!(pipeline: build.pipeline) diff --git a/app/services/packages/nuget/create_package_service.rb b/app/services/packages/nuget/create_package_service.rb index 68ad7f028e4..3999ccd3347 100644 --- a/app/services/packages/nuget/create_package_service.rb +++ b/app/services/packages/nuget/create_package_service.rb @@ -2,12 +2,12 @@ module Packages module Nuget - class CreatePackageService < BaseService + class CreatePackageService < ::Packages::CreatePackageService TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package' PACKAGE_VERSION = '0.0.0' def execute - project.packages.nuget.create!( + create_package!(:nuget, name: TEMPORARY_PACKAGE_NAME, version: "#{PACKAGE_VERSION}-#{uuid}" ) diff --git a/app/services/packages/pypi/create_package_service.rb b/app/services/packages/pypi/create_package_service.rb index 1313fc80e33..e70552e8fa0 100644 --- a/app/services/packages/pypi/create_package_service.rb +++ b/app/services/packages/pypi/create_package_service.rb @@ -2,7 +2,7 @@ module Packages module Pypi - class CreatePackageService < BaseService + class CreatePackageService < ::Packages::CreatePackageService include ::Gitlab::Utils::StrongMemoize def execute @@ -20,10 +20,7 @@ module Packages def created_package strong_memoize(:created_package) do - project - .packages - .pypi - .safe_find_or_create_by!(name: params[:name], version: params[:version]) + find_or_create_package!(:pypi) end end diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 786af3714a6..194b10e9ef4 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -24,4 +24,4 @@ %p.text-secondary = _('Try using a different search term to find the file you are looking for.') .text-center.gl-mt-3.loading - .spinner.spinner-md + = loading_icon(size: 'md') diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 874adb19734..f0a68512326 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -23,7 +23,7 @@ selected: f.object.source_project_id .merge-request-select.dropdown = f.hidden_field :source_branch - = dropdown_toggle f.object.source_branch || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch monospace" } + = dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch monospace" } .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown = dropdown_title(_("Select source branch")) = dropdown_filter(_("Search branches")) @@ -52,7 +52,7 @@ selected: f.object.target_project_id .merge-request-select.dropdown = f.hidden_field :target_branch - = dropdown_toggle f.object.target_branch || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" } + = dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" } .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown = dropdown_title(_("Select target branch")) = dropdown_filter(_("Search branches")) diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 0f6188fa334..96da5136908 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,77 +1,77 @@ = form_errors(hook) .form-group - = form.label :url, 'URL', class: 'label-bold' + = form.label :url, s_('Webhooks|URL'), class: 'label-bold' = form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json' .form-group - = form.label :token, 'Secret Token', class: 'label-bold' + = form.label :token, s_('Webhooks|Secret Token'), class: 'label-bold' = form.text_field :token, class: 'form-control', placeholder: '' %p.form-text.text-muted - Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. + = s_('Webhooks|Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.') .form-group - = form.label :url, 'Trigger', class: 'label-bold' + = form.label :url, s_('Webhooks|Trigger'), class: 'label-bold' %ul.list-unstyled.prepend-left-20 %li = form.check_box :push_events, class: 'form-check-input' = form.label :push_events, class: 'list-label form-check-label ml-1' do - %strong Push events + %strong= s_('Webhooks|Push events') = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)' %p.text-muted.ml-1 - This URL will be triggered by a push to the repository + = s_('Webhooks|This URL will be triggered by a push to the repository') %li = form.check_box :tag_push_events, class: 'form-check-input' = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do - %strong Tag push events + %strong= s_('Webhooks|Tag push events') %p.text-muted.ml-1 - This URL will be triggered when a new tag is pushed to the repository + = s_('Webhooks|This URL will be triggered when a new tag is pushed to the repository') %li = form.check_box :note_events, class: 'form-check-input' = form.label :note_events, class: 'list-label form-check-label ml-1' do - %strong Comments + %strong= s_('Webhooks|Comments') %p.text-muted.ml-1 - This URL will be triggered when someone adds a comment + = s_('Webhooks|This URL will be triggered when someone adds a comment') %li = form.check_box :confidential_note_events, class: 'form-check-input' = form.label :confidential_note_events, class: 'list-label form-check-label ml-1' do - %strong Confidential Comments + %strong= s_('Webhooks|Confidential Comments') %p.text-muted.ml-1 - This URL will be triggered when someone adds a comment on a confidential issue + = s_('Webhooks|This URL will be triggered when someone adds a comment on a confidential issue') %li = form.check_box :issues_events, class: 'form-check-input' = form.label :issues_events, class: 'list-label form-check-label ml-1' do - %strong Issues events + %strong= s_('Webhooks|Issues events') %p.text-muted.ml-1 - This URL will be triggered when an issue is created/updated/merged + = s_('Webhooks|This URL will be triggered when an issue is created/updated/merged') %li = form.check_box :confidential_issues_events, class: 'form-check-input' = form.label :confidential_issues_events, class: 'list-label form-check-label ml-1' do - %strong Confidential Issues events + %strong= s_('Webhooks|Confidential Issues events') %p.text-muted.ml-1 - This URL will be triggered when a confidential issue is created/updated/merged + = s_('Webhooks|This URL will be triggered when a confidential issue is created/updated/merged') %li = form.check_box :merge_requests_events, class: 'form-check-input' = form.label :merge_requests_events, class: 'list-label form-check-label ml-1' do - %strong Merge request events + %strong= s_('Webhooks|Merge request events') %p.text-muted.ml-1 - This URL will be triggered when a merge request is created/updated/merged + = s_('Webhooks|This URL will be triggered when a merge request is created/updated/merged') %li = form.check_box :job_events, class: 'form-check-input' = form.label :job_events, class: 'list-label form-check-label ml-1' do - %strong Job events + %strong= s_('Webhooks|Job events') %p.text-muted.ml-1 - This URL will be triggered when the job status changes + = s_('Webhooks|This URL will be triggered when the job status changes') %li = form.check_box :pipeline_events, class: 'form-check-input' = form.label :pipeline_events, class: 'list-label form-check-label ml-1' do - %strong Pipeline events + %strong= s_('Webhooks|Pipeline events') %p.text-muted.ml-1 - This URL will be triggered when the pipeline status changes + = s_('Webhooks|This URL will be triggered when the pipeline status changes') %li = form.check_box :wiki_page_events, class: 'form-check-input' = form.label :wiki_page_events, class: 'list-label form-check-label ml-1' do - %strong Wiki Page events + %strong= s_('Webhooks|Wiki Page events') %p.text-muted.ml-1 - This URL will be triggered when a wiki page is created/updated + = s_('Webhooks|This URL will be triggered when a wiki page is created/updated') %li = form.check_box :deployment_events, class: 'form-check-input' = form.label :deployment_events, class: 'list-label form-check-label ml-1' do @@ -79,8 +79,8 @@ %p.text-muted.ml-1 = s_('Webhooks|This URL will be triggered when a deployment is finished/failed/canceled') .form-group - = form.label :enable_ssl_verification, 'SSL verification', class: 'label-bold checkbox' + = form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox' .form-check = form.check_box :enable_ssl_verification, class: 'form-check-input' = form.label :enable_ssl_verification, class: 'form-check-label ml-1' do - %strong Enable SSL verification + %strong= s_('Webhooks|Enable SSL verification') diff --git a/app/workers/ci/create_cross_project_pipeline_worker.rb b/app/workers/ci/create_cross_project_pipeline_worker.rb index 713d0092b32..679574d9f60 100644 --- a/app/workers/ci/create_cross_project_pipeline_worker.rb +++ b/app/workers/ci/create_cross_project_pipeline_worker.rb @@ -9,7 +9,7 @@ module Ci def perform(bridge_id) ::Ci::Bridge.find_by_id(bridge_id).try do |bridge| - ::Ci::CreateCrossProjectPipelineService + ::Ci::CreateDownstreamPipelineService .new(bridge.project, bridge.user) .execute(bridge) end diff --git a/changelogs/unreleased/202270-migrate-spinner-for-app-assets-javascripts-notes-components-note_h.yml b/changelogs/unreleased/202270-migrate-spinner-for-app-assets-javascripts-notes-components-note_h.yml new file mode 100644 index 00000000000..c506a08a936 --- /dev/null +++ b/changelogs/unreleased/202270-migrate-spinner-for-app-assets-javascripts-notes-components-note_h.yml @@ -0,0 +1,5 @@ +--- +title: Migrate '.fa-spinner' to '.spinner' for 'app/assets/javascripts/notes/components/note_header.vue' +merge_request: 41140 +author: Gilang Gumilar +type: changed diff --git a/changelogs/unreleased/202582-migrate-spinner-for-app-views-projects-find_file.yml b/changelogs/unreleased/202582-migrate-spinner-for-app-views-projects-find_file.yml new file mode 100644 index 00000000000..0aa4855afe5 --- /dev/null +++ b/changelogs/unreleased/202582-migrate-spinner-for-app-views-projects-find_file.yml @@ -0,0 +1,5 @@ +--- +title: Migrate '.fa-spinner' to '.spinner' for 'app/views/projects/find_file' +merge_request: 41134 +author: Gilang Gumilar +type: changed diff --git a/changelogs/unreleased/232840-fj-track-sfe-actions.yml b/changelogs/unreleased/232840-fj-track-sfe-actions.yml new file mode 100644 index 00000000000..6e32187e941 --- /dev/null +++ b/changelogs/unreleased/232840-fj-track-sfe-actions.yml @@ -0,0 +1,5 @@ +--- +title: Track SFE actions in BlobController +merge_request: 40846 +author: +type: changed diff --git a/changelogs/unreleased/232844-fj-add-ide-actions-usage-data.yml b/changelogs/unreleased/232844-fj-add-ide-actions-usage-data.yml new file mode 100644 index 00000000000..90878f07547 --- /dev/null +++ b/changelogs/unreleased/232844-fj-add-ide-actions-usage-data.yml @@ -0,0 +1,5 @@ +--- +title: Add IDE edit actions to Usage Data +merge_request: 40939 +author: +type: changed diff --git a/changelogs/unreleased/233475-source-branch-dropdown-empty.yml b/changelogs/unreleased/233475-source-branch-dropdown-empty.yml new file mode 100644 index 00000000000..5fed11ec057 --- /dev/null +++ b/changelogs/unreleased/233475-source-branch-dropdown-empty.yml @@ -0,0 +1,5 @@ +--- +title: Show default message in branch selection if none selected +merge_request: 41211 +author: Jonston Chan +type: fixed diff --git a/changelogs/unreleased/235699-graphql-board-issue-filters.yml b/changelogs/unreleased/235699-graphql-board-issue-filters.yml new file mode 100644 index 00000000000..55d1f3d4c6d --- /dev/null +++ b/changelogs/unreleased/235699-graphql-board-issue-filters.yml @@ -0,0 +1,5 @@ +--- +title: Add issue filters when listing board issues in GraphQL +merge_request: 40602 +author: +type: added diff --git a/changelogs/unreleased/i18n-webhook-form.yml b/changelogs/unreleased/i18n-webhook-form.yml new file mode 100644 index 00000000000..5506b11d6f2 --- /dev/null +++ b/changelogs/unreleased/i18n-webhook-form.yml @@ -0,0 +1,5 @@ +--- +title: Externalize i18n strings from app/views/shared/web_hooks/_form.html.haml +merge_request: 41234 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/jdb-refactor-inline-diff-table-row.yml b/changelogs/unreleased/jdb-refactor-inline-diff-table-row.yml new file mode 100644 index 00000000000..9d3d9ea0b0a --- /dev/null +++ b/changelogs/unreleased/jdb-refactor-inline-diff-table-row.yml @@ -0,0 +1,5 @@ +--- +title: Jdb/refactor inline diff table row +merge_request: 40906 +author: +type: performance diff --git a/changelogs/unreleased/let-before-examples-cop.yml b/changelogs/unreleased/let-before-examples-cop.yml new file mode 100644 index 00000000000..fca5fd58f1d --- /dev/null +++ b/changelogs/unreleased/let-before-examples-cop.yml @@ -0,0 +1,5 @@ +--- +title: Fix RSpec/LetBeforeExamples cop +merge_request: 41250 +author: Rajendra Kadam +type: fixed diff --git a/changelogs/unreleased/optimized-issuable-label-search-pt1.yml b/changelogs/unreleased/optimized-issuable-label-search-pt1.yml new file mode 100644 index 00000000000..1c0943a9945 --- /dev/null +++ b/changelogs/unreleased/optimized-issuable-label-search-pt1.yml @@ -0,0 +1,5 @@ +--- +title: Add indexes to `label_links` database table +merge_request: 34503 +author: +type: other diff --git a/changelogs/unreleased/single-line-cop.yml b/changelogs/unreleased/single-line-cop.yml new file mode 100644 index 00000000000..76d5d98282a --- /dev/null +++ b/changelogs/unreleased/single-line-cop.yml @@ -0,0 +1,5 @@ +--- +title: Fix Style/SingleLineMethods cop +merge_request: 41247 +author: Rajendra Kadam +type: fixed diff --git a/config/feature_flags/development/optimized_issuable_label_filter.yml b/config/feature_flags/development/optimized_issuable_label_filter.yml new file mode 100644 index 00000000000..12db63b03fb --- /dev/null +++ b/config/feature_flags/development/optimized_issuable_label_filter.yml @@ -0,0 +1,7 @@ +--- +name: optimized_issuable_label_filter +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34503 +rollout_issue_url: +group: group::analytics +type: development +default_enabled: false diff --git a/db/migrate/20200629134747_add_extra_index_to_label_links.rb b/db/migrate/20200629134747_add_extra_index_to_label_links.rb new file mode 100644 index 00000000000..e2066a1db42 --- /dev/null +++ b/db/migrate/20200629134747_add_extra_index_to_label_links.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class AddExtraIndexToLabelLinks < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_COVERING_ALL_COLUMNS = 'index_on_label_links_all_columns' + INDEX_TO_REPLACE = 'index_label_links_on_label_id' + NEW_INDEX = 'index_label_links_on_label_id_and_target_type' + + disable_ddl_transaction! + + def up + add_concurrent_index :label_links, [:target_id, :label_id, :target_type], name: INDEX_COVERING_ALL_COLUMNS + + add_concurrent_index :label_links, [:label_id, :target_type], name: NEW_INDEX + remove_concurrent_index_by_name(:label_links, INDEX_TO_REPLACE) + end + + def down + remove_concurrent_index_by_name(:label_links, INDEX_COVERING_ALL_COLUMNS) + + add_concurrent_index(:label_links, :label_id, name: INDEX_TO_REPLACE) + remove_concurrent_index_by_name(:label_links, NEW_INDEX) + end +end diff --git a/db/post_migrate/20200826220745_add_compound_index_on_vulnerabilities_for_background_migration.rb b/db/post_migrate/20200826220745_add_compound_index_on_vulnerabilities_for_background_migration.rb new file mode 100644 index 00000000000..da95f708cf9 --- /dev/null +++ b/db/post_migrate/20200826220745_add_compound_index_on_vulnerabilities_for_background_migration.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddCompoundIndexOnVulnerabilitiesForBackgroundMigration < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_vulnerabilities_on_project_id_and_id' + + disable_ddl_transaction! + + def up + add_concurrent_index :vulnerabilities, [:project_id, :id], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :vulnerabilities, INDEX_NAME + end +end diff --git a/db/post_migrate/20200826220746_schedule_populate_resolved_on_default_branch_column.rb b/db/post_migrate/20200826220746_schedule_populate_resolved_on_default_branch_column.rb new file mode 100644 index 00000000000..6faa4fc8101 --- /dev/null +++ b/db/post_migrate/20200826220746_schedule_populate_resolved_on_default_branch_column.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class SchedulePopulateResolvedOnDefaultBranchColumn < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 100 + DELAY_INTERVAL = 5.minutes.to_i + MIGRATION_CLASS = 'PopulateResolvedOnDefaultBranchColumn' + + disable_ddl_transaction! + + def up + return unless Gitlab.ee? + + EE::Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn::Vulnerability.distinct.each_batch(of: BATCH_SIZE, column: :project_id) do |batch, index| + project_ids = batch.pluck(:project_id) + migrate_in(index * DELAY_INTERVAL, MIGRATION_CLASS, project_ids) + end + end + + def down + # no-op + # This migration schedules background tasks to populate + # `resolved_on_default_branch` column of `vulnerabilities` + # table so there is no rollback operation needed for this. + end +end diff --git a/db/schema_migrations/20200629134747 b/db/schema_migrations/20200629134747 new file mode 100644 index 00000000000..04c2c1632b1 --- /dev/null +++ b/db/schema_migrations/20200629134747 @@ -0,0 +1 @@ +9cd0e15dd2c5e70e53fc154a47a76ec066c741b5f6d148972b96d23888f0fcd4
\ No newline at end of file diff --git a/db/schema_migrations/20200826220745 b/db/schema_migrations/20200826220745 new file mode 100644 index 00000000000..39134fb6223 --- /dev/null +++ b/db/schema_migrations/20200826220745 @@ -0,0 +1 @@ +ee38dd60087a8879c4686214da1d25a60ab74306eb07b938efb1a8dfc46cc73a
\ No newline at end of file diff --git a/db/schema_migrations/20200826220746 b/db/schema_migrations/20200826220746 new file mode 100644 index 00000000000..1ff87e8df6d --- /dev/null +++ b/db/schema_migrations/20200826220746 @@ -0,0 +1 @@ +2564c387b727e557b2988996aa533ba5e4e6d7b01515407bd2692c09644ac2be
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b0837b8275a..893cb8d9e31 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20123,7 +20123,7 @@ CREATE INDEX index_keys_on_user_id ON public.keys USING btree (user_id); CREATE UNIQUE INDEX index_kubernetes_namespaces_on_cluster_project_environment_id ON public.clusters_kubernetes_namespaces USING btree (cluster_id, project_id, environment_id); -CREATE INDEX index_label_links_on_label_id ON public.label_links USING btree (label_id); +CREATE INDEX index_label_links_on_label_id_and_target_type ON public.label_links USING btree (label_id, target_type); CREATE INDEX index_label_links_on_target_id_and_target_type ON public.label_links USING btree (target_id, target_type); @@ -20409,6 +20409,8 @@ CREATE INDEX index_on_identities_lower_extern_uid_and_provider ON public.identit CREATE UNIQUE INDEX index_on_instance_statistics_recorded_at_and_identifier ON public.analytics_instance_statistics_measurements USING btree (identifier, recorded_at); +CREATE INDEX index_on_label_links_all_columns ON public.label_links USING btree (target_id, label_id, target_type); + CREATE INDEX index_on_users_name_lower ON public.users USING btree (lower((name)::text)); CREATE INDEX index_open_project_tracker_data_on_service_id ON public.open_project_tracker_data USING btree (service_id); @@ -21173,6 +21175,8 @@ CREATE INDEX index_vulnerabilities_on_milestone_id ON public.vulnerabilities USI CREATE INDEX index_vulnerabilities_on_project_id ON public.vulnerabilities USING btree (project_id); +CREATE INDEX index_vulnerabilities_on_project_id_and_id ON public.vulnerabilities USING btree (project_id, id); + CREATE INDEX index_vulnerabilities_on_resolved_by_id ON public.vulnerabilities USING btree (resolved_by_id); CREATE INDEX index_vulnerabilities_on_start_date_sourcing_milestone_id ON public.vulnerabilities USING btree (start_date_sourcing_milestone_id); diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 7ae683ba705..34d26008972 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1029,7 +1029,7 @@ type Board { """ Filters applied when selecting issues on the board """ - issueFilters: BoardEpicIssueInput + issueFilters: BoardIssueInput """ Returns the last _n_ elements from the list. @@ -1133,7 +1133,12 @@ type BoardEdge { node: Board } -input BoardEpicIssueInput { +""" +Identifier of Board +""" +scalar BoardID + +input BoardIssueInput { """ Filter by assignee username """ @@ -1145,9 +1150,14 @@ input BoardEpicIssueInput { authorUsername: String """ - Filter by epic ID + Filter by epic ID. Incompatible with epicWildcardId + """ + epicId: ID + + """ + Filter by epic ID wildcard. Incompatible with epicId """ - epicId: String + epicWildcardId: EpicWildcardId """ Filter by label name @@ -1167,7 +1177,7 @@ input BoardEpicIssueInput { """ List of negated params. Warning: this argument is experimental and a subject to change in future """ - not: NegatedBoardEpicIssueInput + not: NegatedBoardIssueInput """ Filter by release tag @@ -1186,11 +1196,6 @@ input BoardEpicIssueInput { } """ -Identifier of Board -""" -scalar BoardID - -""" Represents a list for an issue board """ type BoardList { @@ -1224,6 +1229,11 @@ type BoardList { before: String """ + Filters applied when selecting issues in the board list + """ + filters: BoardIssueInput + + """ Returns the first _n_ elements from the list. """ first: Int @@ -5884,6 +5894,21 @@ type EpicTreeReorderPayload { errors: [String!]! } +""" +Epic ID wildcard values +""" +enum EpicWildcardId { + """ + Any epic is assigned + """ + ANY + + """ + No epic is assigned + """ + NONE +} + type GeoNode { """ The maximum concurrency of container repository sync for this secondary node @@ -6864,6 +6889,36 @@ type Group { ): VulnerabilityScannerConnection """ + Counts for each vulnerability severity in the group and its subgroups + """ + vulnerabilitySeveritiesCount( + """ + Filter vulnerabilities by project + """ + projectId: [ID!] + + """ + Filter vulnerabilities by report type + """ + reportType: [VulnerabilityReportType!] + + """ + Filter vulnerabilities by scanner + """ + scanner: [String!] + + """ + Filter vulnerabilities by severity + """ + severity: [VulnerabilitySeverity!] + + """ + Filter vulnerabilities by state + """ + state: [VulnerabilityState!] + ): VulnerabilitySeveritiesCount + + """ Web URL of the group """ webUrl: String! @@ -7030,6 +7085,36 @@ type InstanceSecurityDashboard { """ last: Int ): VulnerabilityScannerConnection + + """ + Counts for each vulnerability severity from projects selected in Instance Security Dashboard + """ + vulnerabilitySeveritiesCount( + """ + Filter vulnerabilities by project + """ + projectId: [ID!] + + """ + Filter vulnerabilities by report type + """ + reportType: [VulnerabilityReportType!] + + """ + Filter vulnerabilities by scanner + """ + scanner: [String!] + + """ + Filter vulnerabilities by severity + """ + severity: [VulnerabilitySeverity!] + + """ + Filter vulnerabilities by state + """ + state: [VulnerabilityState!] + ): VulnerabilitySeveritiesCount } """ @@ -10236,7 +10321,7 @@ type NamespaceIncreaseStorageTemporarilyPayload { namespace: Namespace } -input NegatedBoardEpicIssueInput { +input NegatedBoardIssueInput { """ Filter by assignee username """ @@ -10248,9 +10333,9 @@ input NegatedBoardEpicIssueInput { authorUsername: String """ - Filter by epic ID + Filter by epic ID. Incompatible with epicWildcardId """ - epicId: String + epicId: ID """ Filter by label name @@ -12428,9 +12513,34 @@ type Project { ): VulnerabilityScannerConnection """ - Counts for each severity of vulnerability of the project + Counts for each vulnerability severity in the project """ - vulnerabilitySeveritiesCount: VulnerabilitySeveritiesCount + vulnerabilitySeveritiesCount( + """ + Filter vulnerabilities by project + """ + projectId: [ID!] + + """ + Filter vulnerabilities by report type + """ + reportType: [VulnerabilityReportType!] + + """ + Filter vulnerabilities by scanner + """ + scanner: [String!] + + """ + Filter vulnerabilities by severity + """ + severity: [VulnerabilitySeverity!] + + """ + Filter vulnerabilities by state + """ + state: [VulnerabilityState!] + ): VulnerabilitySeveritiesCount """ Web URL of the project diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 7dab2b15dca..fdffa810044 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -2716,7 +2716,7 @@ "description": "Filters applied when selecting issues on the board", "type": { "kind": "INPUT_OBJECT", - "name": "BoardEpicIssueInput", + "name": "BoardIssueInput", "ofType": null }, "defaultValue": null @@ -3042,8 +3042,18 @@ "possibleTypes": null }, { + "kind": "SCALAR", + "name": "BoardID", + "description": "Identifier of Board", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "INPUT_OBJECT", - "name": "BoardEpicIssueInput", + "name": "BoardIssueInput", "description": null, "fields": null, "inputFields": [ @@ -3106,8 +3116,8 @@ "defaultValue": null }, { - "name": "epicId", - "description": "Filter by epic ID", + "name": "myReactionEmoji", + "description": "Filter by reaction emoji", "type": { "kind": "SCALAR", "name": "String", @@ -3116,11 +3126,11 @@ "defaultValue": null }, { - "name": "myReactionEmoji", - "description": "Filter by reaction emoji", + "name": "epicId", + "description": "Filter by epic ID. Incompatible with epicWildcardId", "type": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null }, "defaultValue": null @@ -3140,7 +3150,7 @@ "description": "List of negated params. Warning: this argument is experimental and a subject to change in future", "type": { "kind": "INPUT_OBJECT", - "name": "NegatedBoardEpicIssueInput", + "name": "NegatedBoardIssueInput", "ofType": null }, "defaultValue": null @@ -3154,6 +3164,16 @@ "ofType": null }, "defaultValue": null + }, + { + "name": "epicWildcardId", + "description": "Filter by epic ID wildcard. Incompatible with epicId", + "type": { + "kind": "ENUM", + "name": "EpicWildcardId", + "ofType": null + }, + "defaultValue": null } ], "interfaces": null, @@ -3161,16 +3181,6 @@ "possibleTypes": null }, { - "kind": "SCALAR", - "name": "BoardID", - "description": "Identifier of Board", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { "kind": "OBJECT", "name": "BoardList", "description": "Represents a list for an issue board", @@ -3226,6 +3236,16 @@ "description": "Board issues", "args": [ { + "name": "filters", + "description": "Filters applied when selecting issues in the board list", + "type": { + "kind": "INPUT_OBJECT", + "name": "BoardIssueInput", + "ofType": null + }, + "defaultValue": null + }, + { "name": "after", "description": "Returns the elements in the list that come after the specified cursor.", "type": { @@ -16458,6 +16478,29 @@ "possibleTypes": null }, { + "kind": "ENUM", + "name": "EpicWildcardId", + "description": "Epic ID wildcard values", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "NONE", + "description": "No epic is assigned", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ANY", + "description": "Any epic is assigned", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { "kind": "SCALAR", "name": "Float", "description": "Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", @@ -18892,6 +18935,109 @@ "deprecationReason": null }, { + "name": "vulnerabilitySeveritiesCount", + "description": "Counts for each vulnerability severity in the group and its subgroups", + "args": [ + { + "name": "projectId", + "description": "Filter vulnerabilities by project", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "reportType", + "description": "Filter vulnerabilities by report type", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityReportType", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "severity", + "description": "Filter vulnerabilities by severity", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilitySeverity", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "state", + "description": "Filter vulnerabilities by state", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityState", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "scanner", + "description": "Filter vulnerabilities by scanner", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VulnerabilitySeveritiesCount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "webUrl", "description": "Web URL of the group", "args": [ @@ -19404,6 +19550,109 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "vulnerabilitySeveritiesCount", + "description": "Counts for each vulnerability severity from projects selected in Instance Security Dashboard", + "args": [ + { + "name": "projectId", + "description": "Filter vulnerabilities by project", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "reportType", + "description": "Filter vulnerabilities by report type", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityReportType", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "severity", + "description": "Filter vulnerabilities by severity", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilitySeverity", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "state", + "description": "Filter vulnerabilities by state", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityState", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "scanner", + "description": "Filter vulnerabilities by scanner", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VulnerabilitySeveritiesCount", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -30628,7 +30877,7 @@ }, { "kind": "INPUT_OBJECT", - "name": "NegatedBoardEpicIssueInput", + "name": "NegatedBoardIssueInput", "description": null, "fields": null, "inputFields": [ @@ -30691,8 +30940,8 @@ "defaultValue": null }, { - "name": "epicId", - "description": "Filter by epic ID", + "name": "myReactionEmoji", + "description": "Filter by reaction emoji", "type": { "kind": "SCALAR", "name": "String", @@ -30701,11 +30950,11 @@ "defaultValue": null }, { - "name": "myReactionEmoji", - "description": "Filter by reaction emoji", + "name": "epicId", + "description": "Filter by epic ID. Incompatible with epicWildcardId", "type": { "kind": "SCALAR", - "name": "String", + "name": "ID", "ofType": null }, "defaultValue": null @@ -36407,9 +36656,98 @@ }, { "name": "vulnerabilitySeveritiesCount", - "description": "Counts for each severity of vulnerability of the project", + "description": "Counts for each vulnerability severity in the project", "args": [ - + { + "name": "projectId", + "description": "Filter vulnerabilities by project", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "reportType", + "description": "Filter vulnerabilities by report type", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityReportType", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "severity", + "description": "Filter vulnerabilities by severity", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilitySeverity", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "state", + "description": "Filter vulnerabilities by state", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VulnerabilityState", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "scanner", + "description": "Filter vulnerabilities by scanner", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null + } ], "type": { "kind": "OBJECT", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7ead6d397a2..9c88d802987 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1048,6 +1048,7 @@ Autogenerated return type of EpicTreeReorder | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource | | `visibility` | String | Visibility of the namespace | | `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade | +| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity in the group and its subgroups | | `webUrl` | String! | Web URL of the group | ## GroupMember @@ -1077,6 +1078,7 @@ Represents a Group Membership | Name | Type | Description | | --- | ---- | ---------- | | `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade | +| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity from projects selected in Instance Security Dashboard | ## Issue @@ -1770,7 +1772,7 @@ Autogenerated return type of PipelineRetry | `tagList` | String | List of project topics (not Git tags) | | `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource | | `visibility` | String | Visibility of the project | -| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each severity of vulnerability of the project | +| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity in the project | | `webUrl` | String | Web URL of the project | | `wikiEnabled` | Boolean | Indicates if Wikis are enabled for the current user | diff --git a/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb b/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb new file mode 100644 index 00000000000..eb72ef1de33 --- /dev/null +++ b/lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # rubocop:disable Style/Documentation + class PopulateResolvedOnDefaultBranchColumn + def perform(*); end + end + end +end + +Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn.prepend_if_ee('EE::Gitlab::BackgroundMigration::PopulateResolvedOnDefaultBranchColumn') diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index e145bd2e9df..1fac00337a3 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -31,105 +31,205 @@ module Gitlab end class Converter - def on_0(_) reset end + def on_0(_) + reset + end - def on_1(_) enable(STYLE_SWITCHES[:bold]) end + def on_1(_) + enable(STYLE_SWITCHES[:bold]) + end - def on_3(_) enable(STYLE_SWITCHES[:italic]) end + def on_3(_) + enable(STYLE_SWITCHES[:italic]) + end - def on_4(_) enable(STYLE_SWITCHES[:underline]) end + def on_4(_) + enable(STYLE_SWITCHES[:underline]) + end - def on_8(_) enable(STYLE_SWITCHES[:conceal]) end + def on_8(_) + enable(STYLE_SWITCHES[:conceal]) + end - def on_9(_) enable(STYLE_SWITCHES[:cross]) end + def on_9(_) + enable(STYLE_SWITCHES[:cross]) + end - def on_21(_) disable(STYLE_SWITCHES[:bold]) end + def on_21(_) + disable(STYLE_SWITCHES[:bold]) + end - def on_22(_) disable(STYLE_SWITCHES[:bold]) end + def on_22(_) + disable(STYLE_SWITCHES[:bold]) + end - def on_23(_) disable(STYLE_SWITCHES[:italic]) end + def on_23(_) + disable(STYLE_SWITCHES[:italic]) + end - def on_24(_) disable(STYLE_SWITCHES[:underline]) end + def on_24(_) + disable(STYLE_SWITCHES[:underline]) + end - def on_28(_) disable(STYLE_SWITCHES[:conceal]) end + def on_28(_) + disable(STYLE_SWITCHES[:conceal]) + end - def on_29(_) disable(STYLE_SWITCHES[:cross]) end + def on_29(_) + disable(STYLE_SWITCHES[:cross]) + end - def on_30(_) set_fg_color(0) end + def on_30(_) + set_fg_color(0) + end - def on_31(_) set_fg_color(1) end + def on_31(_) + set_fg_color(1) + end - def on_32(_) set_fg_color(2) end + def on_32(_) + set_fg_color(2) + end - def on_33(_) set_fg_color(3) end + def on_33(_) + set_fg_color(3) + end - def on_34(_) set_fg_color(4) end + def on_34(_) + set_fg_color(4) + end - def on_35(_) set_fg_color(5) end + def on_35(_) + set_fg_color(5) + end - def on_36(_) set_fg_color(6) end + def on_36(_) + set_fg_color(6) + end - def on_37(_) set_fg_color(7) end + def on_37(_) + set_fg_color(7) + end - def on_38(stack) set_fg_color_256(stack) end + def on_38(stack) + set_fg_color_256(stack) + end - def on_39(_) set_fg_color(9) end + def on_39(_) + set_fg_color(9) + end - def on_40(_) set_bg_color(0) end + def on_40(_) + set_bg_color(0) + end - def on_41(_) set_bg_color(1) end + def on_41(_) + set_bg_color(1) + end - def on_42(_) set_bg_color(2) end + def on_42(_) + set_bg_color(2) + end - def on_43(_) set_bg_color(3) end + def on_43(_) + set_bg_color(3) + end - def on_44(_) set_bg_color(4) end + def on_44(_) + set_bg_color(4) + end - def on_45(_) set_bg_color(5) end + def on_45(_) + set_bg_color(5) + end - def on_46(_) set_bg_color(6) end + def on_46(_) + set_bg_color(6) + end - def on_47(_) set_bg_color(7) end + def on_47(_) + set_bg_color(7) + end - def on_48(stack) set_bg_color_256(stack) end + def on_48(stack) + set_bg_color_256(stack) + end - def on_49(_) set_bg_color(9) end + def on_49(_) + set_bg_color(9) + end - def on_90(_) set_fg_color(0, 'l') end + def on_90(_) + set_fg_color(0, 'l') + end - def on_91(_) set_fg_color(1, 'l') end + def on_91(_) + set_fg_color(1, 'l') + end - def on_92(_) set_fg_color(2, 'l') end + def on_92(_) + set_fg_color(2, 'l') + end - def on_93(_) set_fg_color(3, 'l') end + def on_93(_) + set_fg_color(3, 'l') + end - def on_94(_) set_fg_color(4, 'l') end + def on_94(_) + set_fg_color(4, 'l') + end - def on_95(_) set_fg_color(5, 'l') end + def on_95(_) + set_fg_color(5, 'l') + end - def on_96(_) set_fg_color(6, 'l') end + def on_96(_) + set_fg_color(6, 'l') + end - def on_97(_) set_fg_color(7, 'l') end + def on_97(_) + set_fg_color(7, 'l') + end - def on_99(_) set_fg_color(9, 'l') end + def on_99(_) + set_fg_color(9, 'l') + end - def on_100(_) set_bg_color(0, 'l') end + def on_100(_) + set_bg_color(0, 'l') + end - def on_101(_) set_bg_color(1, 'l') end + def on_101(_) + set_bg_color(1, 'l') + end - def on_102(_) set_bg_color(2, 'l') end + def on_102(_) + set_bg_color(2, 'l') + end - def on_103(_) set_bg_color(3, 'l') end + def on_103(_) + set_bg_color(3, 'l') + end - def on_104(_) set_bg_color(4, 'l') end + def on_104(_) + set_bg_color(4, 'l') + end - def on_105(_) set_bg_color(5, 'l') end + def on_105(_) + set_bg_color(5, 'l') + end - def on_106(_) set_bg_color(6, 'l') end + def on_106(_) + set_bg_color(6, 'l') + end - def on_107(_) set_bg_color(7, 'l') end + def on_107(_) + set_bg_color(7, 'l') + end - def on_109(_) set_bg_color(9, 'l') end + def on_109(_) + set_bg_color(9, 'l') + end attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index b2c296cc3d5..22c344a27f7 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -650,41 +650,37 @@ module Gitlab end def action_monthly_active_users(time_period) - counter = Gitlab::UsageDataCounters::TrackUniqueEvents + date_range = { date_from: time_period[:created_at].first, date_to: time_period[:created_at].last } - project_count = redis_usage_data do - counter.count_unique_events( - event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION, - date_from: time_period[:created_at].first, - date_to: time_period[:created_at].last - ) - end + event_monthly_active_users(date_range) + .merge!(ide_monthly_active_users(date_range)) + end - design_count = redis_usage_data do - counter.count_unique_events( - event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION, - date_from: time_period[:created_at].first, - date_to: time_period[:created_at].last - ) - end + private - wiki_count = redis_usage_data do - counter.count_unique_events( - event_action: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION, - date_from: time_period[:created_at].first, - date_to: time_period[:created_at].last - ) + def event_monthly_active_users(date_range) + data = { + action_monthly_active_users_project_repo: Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION, + action_monthly_active_users_design_management: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION, + action_monthly_active_users_wiki_repo: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION + } + + data.each do |key, event| + data[key] = redis_usage_data { Gitlab::UsageDataCounters::TrackUniqueEvents.count_unique_events(event_action: event, **date_range) } end + end + + def ide_monthly_active_users(date_range) + counter = Gitlab::UsageDataCounters::EditorUniqueCounter { - action_monthly_active_users_project_repo: project_count, - action_monthly_active_users_design_management: design_count, - action_monthly_active_users_wiki_repo: wiki_count + action_monthly_active_users_web_ide_edit: redis_usage_data { counter.count_web_ide_edit_actions(date_range) }, + action_monthly_active_users_sfe_edit: redis_usage_data { counter.count_sfe_edit_actions(date_range) }, + action_monthly_active_users_snippet_editor_edit: redis_usage_data { counter.count_snippet_editor_edit_actions(date_range) }, + action_monthly_active_users_ide_edit: redis_usage_data { counter.count_edit_using_editor(date_range) } } end - private - def distinct_count_service_desk_enabled_projects(time_period) project_creator_id_start = user_minimum_id project_creator_id_finish = user_maximum_id diff --git a/locale/gitlab.pot b/locale/gitlab.pot index df6345cba62..01dd4e7b62e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -27754,12 +27754,90 @@ msgstr "" msgid "Webhooks have moved. They can now be found under the Settings menu." msgstr "" +msgid "Webhooks|Comments" +msgstr "" + +msgid "Webhooks|Confidential Comments" +msgstr "" + +msgid "Webhooks|Confidential Issues events" +msgstr "" + msgid "Webhooks|Deployment events" msgstr "" +msgid "Webhooks|Enable SSL verification" +msgstr "" + +msgid "Webhooks|Issues events" +msgstr "" + +msgid "Webhooks|Job events" +msgstr "" + +msgid "Webhooks|Merge request events" +msgstr "" + +msgid "Webhooks|Pipeline events" +msgstr "" + +msgid "Webhooks|Push events" +msgstr "" + +msgid "Webhooks|SSL verification" +msgstr "" + +msgid "Webhooks|Secret Token" +msgstr "" + +msgid "Webhooks|Tag push events" +msgstr "" + +msgid "Webhooks|This URL will be triggered by a push to the repository" +msgstr "" + +msgid "Webhooks|This URL will be triggered when a confidential issue is created/updated/merged" +msgstr "" + msgid "Webhooks|This URL will be triggered when a deployment is finished/failed/canceled" msgstr "" +msgid "Webhooks|This URL will be triggered when a merge request is created/updated/merged" +msgstr "" + +msgid "Webhooks|This URL will be triggered when a new tag is pushed to the repository" +msgstr "" + +msgid "Webhooks|This URL will be triggered when a wiki page is created/updated" +msgstr "" + +msgid "Webhooks|This URL will be triggered when an issue is created/updated/merged" +msgstr "" + +msgid "Webhooks|This URL will be triggered when someone adds a comment" +msgstr "" + +msgid "Webhooks|This URL will be triggered when someone adds a comment on a confidential issue" +msgstr "" + +msgid "Webhooks|This URL will be triggered when the job status changes" +msgstr "" + +msgid "Webhooks|This URL will be triggered when the pipeline status changes" +msgstr "" + +msgid "Webhooks|Trigger" +msgstr "" + +msgid "Webhooks|URL" +msgstr "" + +msgid "Webhooks|Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header." +msgstr "" + +msgid "Webhooks|Wiki Page events" +msgstr "" + msgid "Wednesday" msgstr "" diff --git a/qa/qa/support/json_formatter.rb b/qa/qa/support/json_formatter.rb index ceafa34b1c2..f6e40436ec8 100644 --- a/qa/qa/support/json_formatter.rb +++ b/qa/qa/support/json_formatter.rb @@ -48,7 +48,7 @@ module QA line_number: line_number.to_i, run_time: example.execution_result.run_time, pending_message: example.execution_result.pending_message, - status_issue: example.metadata[:status_issue], + testcase: example.metadata[:testcase], quarantine: example.metadata[:quarantine], screenshot: example.metadata[:screenshot] } diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 9fee97f938c..b998dee09b2 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -347,6 +347,13 @@ RSpec.describe Projects::BlobController do end end end + + it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do + subject { put :update, params: default_params, format: format } + + let(:target_id) { 'g_edit_by_sfe' } + let(:expected_type) { instance_of(Integer) } + end end describe 'DELETE destroy' do @@ -436,4 +443,32 @@ RSpec.describe Projects::BlobController do end end end + + describe 'POST create' do + let(:user) { create(:user) } + let(:default_params) do + { + namespace_id: project.namespace, + project_id: project, + id: 'master', + branch_name: 'master', + file_name: 'docs/EXAMPLE_FILE', + content: 'Added changes', + commit_message: 'Create CHANGELOG' + } + end + + before do + project.add_developer(user) + + sign_in(user) + end + + it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do + subject { post :create, params: default_params, format: format } + + let(:target_id) { 'g_edit_by_sfe' } + let(:expected_type) { instance_of(Integer) } + end + end end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 4d325f55fb0..e85330b08f6 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -154,9 +154,11 @@ RSpec.describe SearchController do allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) end - it_behaves_like 'tracking unique hll events', :show do - let(:request_params) { { scope: 'projects', search: 'term' } } + it_behaves_like 'tracking unique hll events', :search_track_unique_users do + subject { get :show, params: { scope: 'projects', search: 'term' }, format: format } + let(:target_id) { 'i_search_total' } + let(:expected_type) { instance_of(String) } end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 9cb4f8c9fc1..303bfc59f61 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -330,100 +330,139 @@ RSpec.describe IssuesFinder do end end - context 'filtering by label' do - let(:params) { { label_name: label.title } } + shared_examples ':label_name parameter' do + context 'filtering by label' do + let(:params) { { label_name: label.title } } - it 'returns issues with that label' do - expect(issues).to contain_exactly(issue2) - end + it 'returns issues with that label' do + expect(issues).to contain_exactly(issue2) + end - context 'using NOT' do - let(:params) { { not: { label_name: label.title } } } + context 'using NOT' do + let(:params) { { not: { label_name: label.title } } } - it 'returns issues that do not have that label' do - expect(issues).to contain_exactly(issue1, issue3, issue4) - end + it 'returns issues that do not have that label' do + expect(issues).to contain_exactly(issue1, issue3, issue4) + end - # IssuableFinder first filters using the outer params (the ones not inside the `not` key.) - # Afterwards, it applies the `not` params to that resultset. This means that things inside the `not` param - # do not take precedence over the outer params with the same name. - context 'shadowing the same outside param' do - let(:params) { { label_name: label2.title, not: { label_name: label.title } } } + # IssuableFinder first filters using the outer params (the ones not inside the `not` key.) + # Afterwards, it applies the `not` params to that resultset. This means that things inside the `not` param + # do not take precedence over the outer params with the same name. + context 'shadowing the same outside param' do + let(:params) { { label_name: label2.title, not: { label_name: label.title } } } - it 'does not take precedence over labels outside NOT' do - expect(issues).to contain_exactly(issue3) + it 'does not take precedence over labels outside NOT' do + expect(issues).to contain_exactly(issue3) + end end - end - context 'further filtering outside params' do - let(:params) { { label_name: label2.title, not: { assignee_username: user2.username } } } + context 'further filtering outside params' do + let(:params) { { label_name: label2.title, not: { assignee_username: user2.username } } } - it 'further filters on the returned resultset' do - expect(issues).to be_empty + it 'further filters on the returned resultset' do + expect(issues).to be_empty + end end end end - end - context 'filtering by multiple labels' do - let(:params) { { label_name: [label.title, label2.title].join(',') } } - let(:label2) { create(:label, project: project2) } + context 'filtering by multiple labels' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label2) { create(:label, project: project2) } - before do - create(:label_link, label: label2, target: issue2) - end + before do + create(:label_link, label: label2, target: issue2) + end - it 'returns the unique issues with all those labels' do - expect(issues).to contain_exactly(issue2) - end + it 'returns the unique issues with all those labels' do + expect(issues).to contain_exactly(issue2) + end - context 'using NOT' do - let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } + context 'using NOT' do + let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } - it 'returns issues that do not have any of the labels provided' do - expect(issues).to contain_exactly(issue1, issue4) + it 'returns issues that do not have any of the labels provided' do + expect(issues).to contain_exactly(issue1, issue4) + end end end - end - context 'filtering by a label that includes any or none in the title' do - let(:params) { { label_name: [label.title, label2.title].join(',') } } - let(:label) { create(:label, title: 'any foo', project: project2) } - let(:label2) { create(:label, title: 'bar none', project: project2) } + context 'filtering by a label that includes any or none in the title' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label) { create(:label, title: 'any foo', project: project2) } + let(:label2) { create(:label, title: 'bar none', project: project2) } - before do - create(:label_link, label: label2, target: issue2) - end + before do + create(:label_link, label: label2, target: issue2) + end - it 'returns the unique issues with all those labels' do - expect(issues).to contain_exactly(issue2) + it 'returns the unique issues with all those labels' do + expect(issues).to contain_exactly(issue2) + end + + context 'using NOT' do + let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } + + it 'returns issues that do not have ANY ONE of the labels provided' do + expect(issues).to contain_exactly(issue1, issue4) + end + end end - context 'using NOT' do - let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } + context 'filtering by no label' do + let(:params) { { label_name: described_class::Params::FILTER_NONE } } - it 'returns issues that do not have ANY ONE of the labels provided' do + it 'returns issues with no labels' do expect(issues).to contain_exactly(issue1, issue4) end end - end - context 'filtering by no label' do - let(:params) { { label_name: described_class::Params::FILTER_NONE } } + context 'filtering by any label' do + let(:params) { { label_name: described_class::Params::FILTER_ANY } } - it 'returns issues with no labels' do - expect(issues).to contain_exactly(issue1, issue4) + it 'returns issues that have one or more label' do + create_list(:label_link, 2, label: create(:label, project: project2), target: issue3) + + expect(issues).to contain_exactly(issue2, issue3) + end + end + + context 'when the same label exists on project and group levels' do + let(:issue1) { create(:issue, project: project1) } + let(:issue2) { create(:issue, project: project1) } + + # Skipping validation to reproduce a "real-word" scenario. + # We still have legacy labels on PRD that have the same title on the group and project levels, example: `bug` + let(:project_label) { build(:label, title: 'somelabel', project: project1).tap { |r| r.save!(validate: false) } } + let(:group_label) { create(:group_label, title: 'somelabel', group: project1.group) } + + let(:params) { { label_name: 'somelabel' } } + + before do + create(:label_link, label: group_label, target: issue1) + create(:label_link, label: project_label, target: issue2) + end + + it 'finds both issue records' do + expect(issues).to contain_exactly(issue1, issue2) + end end end - context 'filtering by any label' do - let(:params) { { label_name: described_class::Params::FILTER_ANY } } + context 'when `optimized_issuable_label_filter` feature flag is off' do + before do + stub_feature_flags(optimized_issuable_label_filter: false) + end - it 'returns issues that have one or more label' do - create_list(:label_link, 2, label: create(:label, project: project2), target: issue3) + it_behaves_like ':label_name parameter' + end - expect(issues).to contain_exactly(issue2, issue3) + context 'when `optimized_issuable_label_filter` feature flag is on' do + before do + stub_feature_flags(optimized_issuable_label_filter: true) end + + it_behaves_like ':label_name parameter' end context 'filtering by issue term' do diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 5b86c891e47..4f86323c7c6 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -167,38 +167,56 @@ RSpec.describe MergeRequestsFinder do end end - describe ':label_name parameter' do - let(:common_labels) { create_list(:label, 3) } - let(:distinct_labels) { create_list(:label, 3) } - let(:merge_requests) do - common_attrs = { - source_project: project1, target_project: project1, author: user - } - distinct_labels.map do |label| - labels = [label, *common_labels] - create(:labeled_merge_request, :closed, labels: labels, **common_attrs) + shared_examples ':label_name parameter' do + describe ':label_name parameter' do + let(:common_labels) { create_list(:label, 3) } + let(:distinct_labels) { create_list(:label, 3) } + let(:merge_requests) do + common_attrs = { + source_project: project1, target_project: project1, author: user + } + distinct_labels.map do |label| + labels = [label, *common_labels] + create(:labeled_merge_request, :closed, labels: labels, **common_attrs) + end end - end - def find(label_name) - described_class.new(user, label_name: label_name).execute - end + def find(label_name) + described_class.new(user, label_name: label_name).execute + end + + it 'accepts a single label' do + found = find(distinct_labels.first.title) + common = find(common_labels.first.title) + + expect(found).to contain_exactly(merge_requests.first) + expect(common).to match_array(merge_requests) + end - it 'accepts a single label' do - found = find(distinct_labels.first.title) - common = find(common_labels.first.title) + it 'accepts an array of labels, all of which must match' do + all_distinct = find(distinct_labels.pluck(:title)) + all_common = find(common_labels.pluck(:title)) - expect(found).to contain_exactly(merge_requests.first) - expect(common).to match_array(merge_requests) + expect(all_distinct).to be_empty + expect(all_common).to match_array(merge_requests) + end + end + end + + context 'when `optimized_issuable_label_filter` feature flag is off' do + before do + stub_feature_flags(optimized_issuable_label_filter: false) end - it 'accepts an array of labels, all of which must match' do - all_distinct = find(distinct_labels.pluck(:title)) - all_common = find(common_labels.pluck(:title)) + it_behaves_like ':label_name parameter' + end - expect(all_distinct).to be_empty - expect(all_common).to match_array(merge_requests) + context 'when `optimized_issuable_label_filter` feature flag is on' do + before do + stub_feature_flags(optimized_issuable_label_filter: true) end + + it_behaves_like ':label_name parameter' end it 'filters by source project id' do diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js index f929f97b598..951b3f6258b 100644 --- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js +++ b/spec/frontend/diffs/components/inline_diff_table_row_spec.js @@ -1,114 +1,317 @@ import { shallowMount } from '@vue/test-utils'; +import { TEST_HOST } from 'helpers/test_constants'; import { createStore } from '~/mr_notes/stores'; import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue'; +import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; import diffFileMockData from '../mock_data/diff_file'; +import discussionsMockData from '../mock_data/diff_discussions'; + +const TEST_USER_ID = 'abc123'; +const TEST_USER = { id: TEST_USER_ID }; describe('InlineDiffTableRow', () => { let wrapper; - let vm; + let store; const thisLine = diffFileMockData.highlighted_diff_lines[0]; - beforeEach(() => { + const createComponent = (props = {}, propsStore = store) => { wrapper = shallowMount(InlineDiffTableRow, { - store: createStore(), + store: propsStore, propsData: { line: thisLine, fileHash: diffFileMockData.file_hash, filePath: diffFileMockData.file_path, contextLinesPath: 'contextLinesPath', isHighlighted: false, + ...props, }, }); - vm = wrapper.vm; + }; + + const setWindowLocation = value => { + Object.defineProperty(window, 'location', { + writable: true, + value, + }); + }; + + beforeEach(() => { + store = createStore(); + store.state.notes.userData = TEST_USER; + }); + + afterEach(() => { + wrapper.destroy(); }); - it('does not add hll class to line content when line does not match highlighted row', done => { - vm.$nextTick() - .then(() => { - expect(wrapper.find('.line_content').classes('hll')).toBe(false); - }) - .then(done) - .catch(done.fail); + it('does not add hll class to line content when line does not match highlighted row', () => { + createComponent(); + expect(wrapper.find('.line_content').classes('hll')).toBe(false); }); - it('adds hll class to lineContent when line is the highlighted row', done => { - vm.$nextTick() - .then(() => { - vm.$store.state.diffs.highlightedRow = thisLine.line_code; - - return vm.$nextTick(); - }) - .then(() => { - expect(wrapper.find('.line_content').classes('hll')).toBe(true); - }) - .then(done) - .catch(done.fail); + it('adds hll class to lineContent when line is the highlighted row', () => { + store.state.diffs.highlightedRow = thisLine.line_code; + createComponent({}, store); + expect(wrapper.find('.line_content').classes('hll')).toBe(true); }); it('adds hll class to lineContent when line is part of a multiline comment', () => { - wrapper.setProps({ isCommented: true }); - return vm.$nextTick().then(() => { - expect(wrapper.find('.line_content').classes('hll')).toBe(true); - }); + createComponent({ isCommented: true }); + expect(wrapper.find('.line_content').classes('hll')).toBe(true); }); describe('sets coverage title and class', () => { - it('for lines with coverage', done => { - vm.$nextTick() - .then(() => { - const name = diffFileMockData.file_path; - const line = thisLine.new_line; - - vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } }; - - return vm.$nextTick(); - }) - .then(() => { - const coverage = wrapper.find('.line-coverage'); - - expect(coverage.attributes('title')).toContain('Test coverage: 5 hits'); - expect(coverage.classes('coverage')).toBe(true); - }) - .then(done) - .catch(done.fail); + it('for lines with coverage', () => { + const name = diffFileMockData.file_path; + const line = thisLine.new_line; + + store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } }; + createComponent({}, store); + const coverage = wrapper.find('.line-coverage'); + + expect(coverage.attributes('title')).toContain('Test coverage: 5 hits'); + expect(coverage.classes('coverage')).toBe(true); + }); + + it('for lines without coverage', () => { + const name = diffFileMockData.file_path; + const line = thisLine.new_line; + + store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } }; + createComponent({}, store); + const coverage = wrapper.find('.line-coverage'); + + expect(coverage.attributes('title')).toContain('No test coverage'); + expect(coverage.classes('no-coverage')).toBe(true); + }); + + it('for unknown lines', () => { + store.state.diffs.coverageFiles = {}; + createComponent({}, store); + + const coverage = wrapper.find('.line-coverage'); + + expect(coverage.attributes('title')).toBeUndefined(); + expect(coverage.classes('coverage')).toBe(false); + expect(coverage.classes('no-coverage')).toBe(false); + }); + }); + + describe('Table Cells', () => { + const findNewTd = () => wrapper.find({ ref: 'newTd' }); + const findOldTd = () => wrapper.find({ ref: 'oldTd' }); + + describe('td', () => { + it('highlights when isHighlighted true', () => { + store.state.diffs.highlightedRow = thisLine.line_code; + createComponent({}, store); + + expect(findNewTd().classes()).toContain('hll'); + expect(findOldTd().classes()).toContain('hll'); + }); + + it('does not highlight when isHighlighted false', () => { + createComponent(); + + expect(findNewTd().classes()).not.toContain('hll'); + expect(findOldTd().classes()).not.toContain('hll'); + }); + }); + + describe('comment button', () => { + const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' }); + + it.each` + userData | query | mergeRefHeadComments | expectation + ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true} + ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true} + ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false} + ${null} | ${''} | ${true} | ${false} + `( + 'exists is $expectation - with userData ($userData) query ($query)', + ({ userData, query, mergeRefHeadComments, expectation }) => { + store.state.notes.userData = userData; + gon.features = { mergeRefHeadComments }; + setWindowLocation({ href: `${TEST_HOST}?${query}` }); + createComponent({}, store); + + expect(findNoteButton().exists()).toBe(expectation); + }, + ); + + it.each` + isHover | line | expectation + ${true} | ${{ ...thisLine, discussions: [] }} | ${true} + ${false} | ${{ ...thisLine, discussions: [] }} | ${false} + ${true} | ${{ ...thisLine, type: 'context', discussions: [] }} | ${false} + ${true} | ${{ ...thisLine, type: 'old-nonewline', discussions: [] }} | ${false} + ${true} | ${{ ...thisLine, discussions: [{}] }} | ${false} + `('visible is $expectation - line ($line)', ({ isHover, line, expectation }) => { + createComponent({ line }); + wrapper.setData({ isHover }); + + return wrapper.vm.$nextTick().then(() => { + expect(findNoteButton().isVisible()).toBe(expectation); + }); + }); + + it.each` + disabled | commentsDisabled + ${'disabled'} | ${true} + ${undefined} | ${false} + `( + 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled', + ({ disabled, commentsDisabled }) => { + createComponent({ + line: { ...thisLine, commentsDisabled }, + }); + + wrapper.setData({ isHover: true }); + + return wrapper.vm.$nextTick().then(() => { + expect(findNoteButton().attributes('disabled')).toBe(disabled); + }); + }, + ); + + const symlinkishFileTooltip = + 'Commenting on symbolic links that replace or are replaced by files is currently not supported.'; + const realishFileTooltip = + 'Commenting on files that replace or are replaced by symbolic links is currently not supported.'; + const otherFileTooltip = 'Add a comment to this line'; + const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' }); + + it.each` + tooltip | commentsDisabled + ${symlinkishFileTooltip} | ${{ wasSymbolic: true }} + ${symlinkishFileTooltip} | ${{ isSymbolic: true }} + ${realishFileTooltip} | ${{ wasReal: true }} + ${realishFileTooltip} | ${{ isReal: true }} + ${otherFileTooltip} | ${false} + `( + 'has the correct tooltip when commentsDisabled=$commentsDisabled', + ({ tooltip, commentsDisabled }) => { + createComponent({ + line: { ...thisLine, commentsDisabled }, + }); + + wrapper.setData({ isHover: true }); + + return wrapper.vm.$nextTick().then(() => { + expect(findTooltip().attributes('title')).toBe(tooltip); + }); + }, + ); }); - it('for lines without coverage', done => { - vm.$nextTick() - .then(() => { - const name = diffFileMockData.file_path; - const line = thisLine.new_line; + describe('line number', () => { + const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' }); + const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' }); + + it('renders line numbers in correct cells', () => { + createComponent(); + + expect(findLineNumberOld().exists()).toBe(false); + expect(findLineNumberNew().exists()).toBe(true); + }); - vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } }; + describe('with lineNumber prop', () => { + const TEST_LINE_CODE = 'LC_42'; + const TEST_LINE_NUMBER = 1; - return vm.$nextTick(); - }) - .then(() => { - const coverage = wrapper.find('.line-coverage'); + describe.each` + lineProps | findLineNumber | expectedHref | expectedClickArg + ${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE} + ${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined} + ${{ line_code: undefined, left: { line_code: TEST_LINE_CODE }, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${TEST_LINE_CODE} + ${{ line_code: undefined, right: { line_code: TEST_LINE_CODE }, new_line: TEST_LINE_NUMBER }} | ${findLineNumberNew} | ${'#'} | ${TEST_LINE_CODE} + `( + 'with line ($lineProps)', + ({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => { + beforeEach(() => { + jest.spyOn(store, 'dispatch').mockImplementation(); + createComponent({ + line: { ...thisLine, ...lineProps }, + }); + }); - expect(coverage.attributes('title')).toContain('No test coverage'); - expect(coverage.classes('no-coverage')).toBe(true); - }) - .then(done) - .catch(done.fail); + it('renders', () => { + expect(findLineNumber().exists()).toBe(true); + expect(findLineNumber().attributes()).toEqual({ + href: expectedHref, + 'data-linenumber': TEST_LINE_NUMBER.toString(), + }); + }); + + it('on click, dispatches setHighlightedRow', () => { + expect(store.dispatch).toHaveBeenCalledTimes(1); + + findLineNumber().trigger('click'); + + expect(store.dispatch).toHaveBeenCalledWith( + 'diffs/setHighlightedRow', + expectedClickArg, + ); + expect(store.dispatch).toHaveBeenCalledTimes(2); + }); + }, + ); + }); }); - it('for unknown lines', done => { - vm.$nextTick() - .then(() => { - vm.$store.state.diffs.coverageFiles = {}; - - return vm.$nextTick(); - }) - .then(() => { - const coverage = wrapper.find('.line-coverage'); - - expect(coverage.attributes('title')).toBeUndefined(); - expect(coverage.classes('coverage')).toBe(false); - expect(coverage.classes('no-coverage')).toBe(false); - }) - .then(done) - .catch(done.fail); + describe('diff-gutter-avatars', () => { + const TEST_LINE_CODE = 'LC_42'; + const TEST_FILE_HASH = diffFileMockData.file_hash; + const findAvatars = () => wrapper.find(DiffGutterAvatars); + let line; + + beforeEach(() => { + jest.spyOn(store, 'dispatch').mockImplementation(); + + line = { + line_code: TEST_LINE_CODE, + type: 'new', + old_line: null, + new_line: 1, + discussions: [{ ...discussionsMockData }], + discussionsExpanded: true, + text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', + meta_data: null, + }; + }); + + describe('with showCommentButton', () => { + it('renders if line has discussions', () => { + createComponent({ line }); + + expect(findAvatars().props()).toEqual({ + discussions: line.discussions, + discussionsExpanded: line.discussionsExpanded, + }); + }); + + it('does notrender if line has no discussions', () => { + line.discussions = []; + createComponent({ line }); + + expect(findAvatars().exists()).toEqual(false); + }); + + it('toggles line discussion', () => { + createComponent({ line }); + + expect(store.dispatch).toHaveBeenCalledTimes(1); + + findAvatars().vm.$emit('toggleLineDiscussions'); + + expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', { + lineCode: TEST_LINE_CODE, + fileHash: TEST_FILE_HASH, + expanded: !line.discussionsExpanded, + }); + }); + }); }); }); }); diff --git a/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap index 172b8919673..4f376a5d58f 100644 --- a/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap +++ b/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap @@ -16,7 +16,7 @@ exports[`Package code instruction single line to match the default snapshot 1`] class="input-group gl-mb-3" > <input - class="form-control monospace js-instruction-input" + class="form-control gl-font-monospace js-instruction-input" readonly="readonly" type="text" /> diff --git a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap index 28b7ca442eb..df2844eaa3c 100644 --- a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap +++ b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap @@ -21,7 +21,7 @@ exports[`DependencyRow renders full dependency 1`] = ` </div> <div - class="table-section section-50 gl-display-flex justify-content-md-end" + class="table-section section-50 gl-display-flex gl-justify-content-md-end" data-testid="version-pattern" > <span diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap index 2b7a4c83bed..6ff9376565a 100644 --- a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -444,7 +444,7 @@ exports[`packages_list_app renders 1`] = ` </template> <template> <div - class="d-flex align-self-center ml-md-auto py-1 py-md-0" + class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end" > <package-filter-stub class="mr-1" diff --git a/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap index 490aa34766c..55ac86f9c49 100644 --- a/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap +++ b/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap @@ -2,35 +2,37 @@ exports[`publish_method renders 1`] = ` <div - class="d-flex align-items-center order-1 order-md-0 mb-md-1" + class="gl-display-flex gl-align-items-center gl-mb-2" > <gl-icon-stub - class="mr-1" + class="gl-mr-2" name="git-merge" size="16" /> <span - class="mr-1" + class="gl-mr-2" + data-testid="pipeline-ref" > branch-name </span> <gl-icon-stub - class="mr-1" + class="gl-mr-2" name="commit" size="16" /> <gl-link-stub - class="mr-1" + class="gl-mr-2" + data-testid="pipeline-sha" href="../commit/sha-baz" > sha-baz </gl-link-stub> <clipboard-button-stub - cssclass="border-0 py-0 px-1" + cssclass="gl-border-0 gl-py-0 gl-px-2" text="sha-baz" title="Copy commit SHA" tooltipplacement="top" diff --git a/spec/frontend/packages/shared/components/publish_method_spec.js b/spec/frontend/packages/shared/components/publish_method_spec.js index bb9287c1204..6014774990c 100644 --- a/spec/frontend/packages/shared/components/publish_method_spec.js +++ b/spec/frontend/packages/shared/components/publish_method_spec.js @@ -7,9 +7,9 @@ describe('publish_method', () => { const [packageWithoutPipeline, packageWithPipeline] = packageList; - const findPipelineRef = () => wrapper.find({ ref: 'pipeline-ref' }); - const findPipelineSha = () => wrapper.find({ ref: 'pipeline-sha' }); - const findManualPublish = () => wrapper.find({ ref: 'manual-ref' }); + const findPipelineRef = () => wrapper.find('[data-testid="pipeline-ref"]'); + const findPipelineSha = () => wrapper.find('[data-testid="pipeline-sha"]'); + const findManualPublish = () => wrapper.find('[data-testid="manually-published"]'); const mountComponent = (packageEntity = {}, isGroup = false) => { wrapper = shallowMount(PublishMethod, { diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js index a036588596a..ccceb78f2d1 100644 --- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js +++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js @@ -2,7 +2,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue'; import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue'; -import store from '~/reports/accessibility_report/store'; +import { getStoreConfig } from '~/reports/accessibility_report/store'; import { mockReport } from './mock_data'; const localVue = createLocalVue(); @@ -20,16 +20,17 @@ describe('Grouped accessibility reports app', () => { propsData: { endpoint: 'endpoint.json', }, - methods: { - fetchReport: () => {}, - }, }); }; const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]'); beforeEach(() => { - mockStore = store(); + mockStore = new Vuex.Store({ + ...getStoreConfig(), + actions: { fetchReport: () => {}, setEndpoint: () => {} }, + }); + mountComponent(); }); diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js index c9bd040aaf5..77d7c6f8678 100644 --- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js +++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js @@ -2,7 +2,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue'; import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue'; -import store from '~/reports/codequality_report/store'; +import { getStoreConfig } from '~/reports/codequality_report/store'; import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data'; const localVue = createLocalVue(); @@ -13,21 +13,22 @@ describe('Grouped code quality reports app', () => { let wrapper; let mockStore; + const PATHS = { + codequalityHelpPath: 'codequality_help.html', + basePath: 'base.json', + headPath: 'head.json', + baseBlobPath: 'base/blob/path/', + headBlobPath: 'head/blob/path/', + }; + const mountComponent = (props = {}) => { wrapper = mount(Component, { store: mockStore, localVue, propsData: { - basePath: 'base.json', - headPath: 'head.json', - baseBlobPath: 'base/blob/path/', - headBlobPath: 'head/blob/path/', - codequalityHelpPath: 'codequality_help.html', + ...PATHS, ...props, }, - methods: { - fetchReports: () => {}, - }, }); }; @@ -35,7 +36,19 @@ describe('Grouped code quality reports app', () => { const findIssueBody = () => wrapper.find(CodequalityIssueBody); beforeEach(() => { - mockStore = store(); + const { state, ...storeConfig } = getStoreConfig(); + mockStore = new Vuex.Store({ + ...storeConfig, + actions: { + setPaths: () => {}, + fetchReports: () => {}, + }, + state: { + ...state, + ...PATHS, + }, + }); + mountComponent(); }); diff --git a/spec/frontend/reports/components/grouped_test_reports_app_spec.js b/spec/frontend/reports/components/grouped_test_reports_app_spec.js index c26e2fbc19a..556904b7da5 100644 --- a/spec/frontend/reports/components/grouped_test_reports_app_spec.js +++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js @@ -1,7 +1,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import GroupedTestReportsApp from '~/reports/components/grouped_test_reports_app.vue'; -import store from '~/reports/store'; +import { getStoreConfig } from '~/reports/store'; import { failedReport } from '../mock_data/mock_data'; import successTestReports from '../mock_data/no_failures_report.json'; @@ -29,9 +29,6 @@ describe('Grouped test reports app', () => { pipelinePath, ...props, }, - methods: { - fetchReports: () => {}, - }, }); }; @@ -49,7 +46,13 @@ describe('Grouped test reports app', () => { wrapper.findAll('[data-testid="test-issue-body-description"]'); beforeEach(() => { - mockStore = store(); + mockStore = new Vuex.Store({ + ...getStoreConfig(), + actions: { + fetchReports: () => {}, + setEndpoint: () => {}, + }, + }); mountComponent(); }); diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb index e23a37b3d69..4ccf194522f 100644 --- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb +++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb @@ -11,41 +11,59 @@ RSpec.describe Resolvers::BoardListIssuesResolver do let_it_be(:group) { create(:group, :private) } shared_examples_for 'group and project board list issues resolver' do - let!(:board) { create(:board, resource_parent: board_parent) } - before do board_parent.add_developer(user) end # auth is handled by the parent object context 'when authorized' do - let!(:list) { create(:list, board: board, label: label) } + let!(:issue1) { create(:issue, project: project, labels: [label], relative_position: 10) } + let!(:issue2) { create(:issue, project: project, labels: [label, label2], relative_position: 12) } + let!(:issue3) { create(:issue, project: project, labels: [label, label3], relative_position: 10) } it 'returns the issues in the correct order' do - issue1 = create(:issue, project: project, labels: [label], relative_position: 10) - issue2 = create(:issue, project: project, labels: [label], relative_position: 12) - issue3 = create(:issue, project: project, labels: [label], relative_position: 10) - # by relative_position and then ID issues = resolve_board_list_issues.items expect(issues.map(&:id)).to eq [issue3.id, issue1.id, issue2.id] end + + it 'finds only issues matching filters' do + result = resolve_board_list_issues(args: { filters: { label_name: label.title, not: { label_name: label2.title } } }).items + + expect(result).to match_array([issue1, issue3]) + end + + it 'finds only issues matching search param' do + result = resolve_board_list_issues(args: { filters: { search: issue1.title } }).items + + expect(result).to match_array([issue1]) + end end end describe '#resolve' do context 'when project boards' do + let_it_be(:label) { create(:label, project: user_project) } + let_it_be(:label2) { create(:label, project: user_project) } + let_it_be(:label3) { create(:label, project: user_project) } + let_it_be(:board) { create(:board, resource_parent: user_project) } + let_it_be(:list) { create(:list, board: board, label: label) } + let(:board_parent) { user_project } - let!(:label) { create(:label, project: project, name: 'project label') } let(:project) { user_project } it_behaves_like 'group and project board list issues resolver' end context 'when group boards' do + let_it_be(:label) { create(:group_label, group: group) } + let_it_be(:label2) { create(:group_label, group: group) } + let_it_be(:label3) { create(:group_label, group: group) } + let_it_be(:board) { create(:board, resource_parent: group) } + let_it_be(:list) { create(:list, board: board, label: label) } + let(:board_parent) { group } - let!(:label) { create(:group_label, group: group, name: 'group label') } let!(:project) { create(:project, :private, group: group) } it_behaves_like 'group and project board list issues resolver' diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index a548589d269..aecffc487aa 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do let_it_be(:merge_request_3) { create(:merge_request, :unique_branches, **common_attrs) } let_it_be(:merge_request_4) { create(:merge_request, :unique_branches, :locked, **common_attrs) } let_it_be(:merge_request_5) { create(:merge_request, :simple, :locked, **common_attrs) } - let_it_be(:merge_request_6) { create(:labeled_merge_request, :unique_branches, labels: create_list(:label, 2), **common_attrs) } + let_it_be(:merge_request_6) { create(:labeled_merge_request, :unique_branches, labels: create_list(:label, 2, project: project), **common_attrs) } let_it_be(:merge_request_with_milestone) { create(:merge_request, :unique_branches, **common_attrs, milestone: milestone) } let_it_be(:other_project) { create(:project, :repository) } let_it_be(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) } diff --git a/spec/graphql/types/boards/board_issue_input_type_spec.rb b/spec/graphql/types/boards/board_issue_input_type_spec.rb new file mode 100644 index 00000000000..6319ff9a88e --- /dev/null +++ b/spec/graphql/types/boards/board_issue_input_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['BoardIssueInput'] do + it { expect(described_class.graphql_name).to eq('BoardIssueInput') } + + it 'exposes negated issue arguments' do + allowed_args = %w(labelName milestoneTitle assigneeUsername authorUsername + releaseTag myReactionEmoji not search) + + expect(described_class.arguments.keys).to include(*allowed_args) + expect(described_class.arguments['not'].type).to eq(Types::Boards::NegatedBoardIssueInputType) + end +end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 0ad058675fe..447802d18a7 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -40,10 +40,10 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do end context 'internal reference' do - it_behaves_like 'a reference containing an element node' - let(:reference) { "##{issue.iid}" } + it_behaves_like 'a reference containing an element node' + it 'links to a valid reference' do doc = reference_filter("Fixed #{reference}") @@ -134,11 +134,11 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do end context 'cross-project / cross-namespace complete reference' do - it_behaves_like 'a reference containing an element node' - - let(:project2) { create(:project, :public) } - let(:issue) { create(:issue, project: project2) } let(:reference) { "#{project2.full_path}##{issue.iid}" } + let(:issue) { create(:issue, project: project2) } + let(:project2) { create(:project, :public) } + + it_behaves_like 'a reference containing an element node' it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) @@ -182,13 +182,13 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do end context 'cross-project / same-namespace complete reference' do - it_behaves_like 'a reference containing an element node' - - let(:namespace) { create(:namespace) } - let(:project) { create(:project, :public, namespace: namespace) } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } let(:reference) { "#{project2.full_path}##{issue.iid}" } + let(:issue) { create(:issue, project: project2) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:namespace) { create(:namespace) } + + it_behaves_like 'a reference containing an element node' it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) @@ -232,13 +232,13 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do end context 'cross-project shorthand reference' do - it_behaves_like 'a reference containing an element node' - - let(:namespace) { create(:namespace) } - let(:project) { create(:project, :public, namespace: namespace) } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } let(:reference) { "#{project2.path}##{issue.iid}" } + let(:issue) { create(:issue, project: project2) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:project) { create(:project, :public, namespace: namespace) } + let(:namespace) { create(:namespace) } + + it_behaves_like 'a reference containing an element node' it 'ignores valid references when cross-reference project uses external tracker' do expect_any_instance_of(described_class).to receive(:find_object) @@ -282,12 +282,12 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do end context 'cross-project URL reference' do - it_behaves_like 'a reference containing an element node' - - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } let(:reference) { issue_url + "#note_123" } + let(:issue) { create(:issue, project: project2) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:namespace) { create(:namespace, name: 'cross-reference') } + + it_behaves_like 'a reference containing an element node' it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -310,13 +310,13 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do end context 'cross-project reference in link href' do - it_behaves_like 'a reference containing an element node' - - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } - let(:reference) { issue.to_reference(project) } let(:reference_link) { %{<a href="#{reference}">Reference</a>} } + let(:reference) { issue.to_reference(project) } + let(:issue) { create(:issue, project: project2) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:namespace) { create(:namespace, name: 'cross-reference') } + + it_behaves_like 'a reference containing an element node' it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") @@ -339,13 +339,13 @@ RSpec.describe Banzai::Filter::IssueReferenceFilter do end context 'cross-project URL in link href' do - it_behaves_like 'a reference containing an element node' - - let(:namespace) { create(:namespace, name: 'cross-reference') } - let(:project2) { create(:project, :public, namespace: namespace) } - let(:issue) { create(:issue, project: project2) } - let(:reference) { "#{issue_url + "#note_123"}" } let(:reference_link) { %{<a href="#{reference}">Reference</a>} } + let(:reference) { "#{issue_url + "#note_123"}" } + let(:issue) { create(:issue, project: project2) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:namespace) { create(:namespace, name: 'cross-reference') } + + it_behaves_like 'a reference containing an element node' it 'links to a valid reference' do doc = reference_filter("See #{reference_link}") diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index d8de3e5cc11..b8baccf6658 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -40,10 +40,10 @@ RSpec.describe Banzai::Filter::UserReferenceFilter do end context 'mentioning @all' do - it_behaves_like 'a reference containing an element node' - let(:reference) { User.reference_prefix + 'all' } + it_behaves_like 'a reference containing an element node' + before do project.add_developer(project.creator) end @@ -78,10 +78,10 @@ RSpec.describe Banzai::Filter::UserReferenceFilter do end context 'mentioning a group' do - it_behaves_like 'a reference containing an element node' - - let(:group) { create(:group) } let(:reference) { group.to_reference } + let(:group) { create(:group) } + + it_behaves_like 'a reference containing an element node' it 'links to the Group' do doc = reference_filter("Hey #{reference}") @@ -98,10 +98,10 @@ RSpec.describe Banzai::Filter::UserReferenceFilter do end context 'mentioning a nested group' do - it_behaves_like 'a reference containing an element node' - - let(:group) { create(:group, :nested) } let(:reference) { group.to_reference } + let(:group) { create(:group, :nested) } + + it_behaves_like 'a reference containing an element node' it 'links to the nested group' do doc = reference_filter("Hey #{reference}") diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index ee2173a9c8d..1a7d837af73 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -4,17 +4,6 @@ require 'spec_helper' RSpec.describe Gitlab::Email::Handler::CreateIssueHandler do include_context :email_shared_context - it_behaves_like :reply_processing_shared_examples - - before do - stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") - stub_config_setting(host: 'localhost') - end - - let(:email_raw) { email_fixture('emails/valid_new_issue.eml') } - let(:namespace) { create(:namespace, path: 'gitlabhq') } - - let!(:project) { create(:project, :public, namespace: namespace, path: 'gitlabhq') } let!(:user) do create( :user, @@ -23,6 +12,17 @@ RSpec.describe Gitlab::Email::Handler::CreateIssueHandler do ) end + let!(:project) { create(:project, :public, namespace: namespace, path: 'gitlabhq') } + let(:namespace) { create(:namespace, path: 'gitlabhq') } + let(:email_raw) { email_fixture('emails/valid_new_issue.eml') } + + it_behaves_like :reply_processing_shared_examples + + before do + stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + context "when email key" do let(:mail) { Mail::Message.new(email_raw) } diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb index 75d5fc040cb..37ee4591db0 100644 --- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb @@ -4,6 +4,18 @@ require 'spec_helper' RSpec.describe Gitlab::Email::Handler::CreateMergeRequestHandler do include_context :email_shared_context + let!(:user) do + create( + :user, + email: 'jake@adventuretime.ooo', + incoming_email_token: 'auth_token' + ) + end + + let!(:project) { create(:project, :public, :repository, namespace: namespace, path: 'gitlabhq') } + let(:namespace) { create(:namespace, path: 'gitlabhq') } + let(:email_raw) { email_fixture('emails/valid_new_merge_request.eml') } + it_behaves_like :reply_processing_shared_examples before do @@ -15,18 +27,6 @@ RSpec.describe Gitlab::Email::Handler::CreateMergeRequestHandler do TestEnv.clean_test_path end - let(:email_raw) { email_fixture('emails/valid_new_merge_request.eml') } - let(:namespace) { create(:namespace, path: 'gitlabhq') } - - let!(:project) { create(:project, :public, :repository, namespace: namespace, path: 'gitlabhq') } - let!(:user) do - create( - :user, - email: 'jake@adventuretime.ooo', - incoming_email_token: 'auth_token' - ) - end - context "when email key" do let(:mail) { Mail::Message.new(email_raw) } diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index e5598bbd10f..07b8070be30 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -4,6 +4,16 @@ require 'spec_helper' RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do include_context :email_shared_context + let!(:sent_notification) do + SentNotification.record_note(note, user.id, mail_key) + end + + let(:noteable) { note.noteable } + let(:note) { create(:diff_note_on_merge_request, project: project) } + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let(:email_raw) { fixture_file('emails/valid_reply.eml') } + it_behaves_like :reply_processing_shared_examples before do @@ -11,16 +21,6 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do stub_config_setting(host: 'localhost') end - let(:email_raw) { fixture_file('emails/valid_reply.eml') } - let(:project) { create(:project, :public, :repository) } - let(:user) { create(:user) } - let(:note) { create(:diff_note_on_merge_request, project: project) } - let(:noteable) { note.noteable } - - let!(:sent_notification) do - SentNotification.record_note(note, user.id, mail_key) - end - context "when the recipient address doesn't include a mail key" do let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") } diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index c80e15055c6..a119c85401d 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -999,6 +999,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do describe '#action_monthly_active_users', :clean_gitlab_redis_shared_state do let(:time_period) { { created_at: 2.days.ago..time } } let(:time) { Time.zone.now } + let(:user1) { build(:user, id: 1) } + let(:user2) { build(:user, id: 2) } + let(:user3) { build(:user, id: 3) } before do counter = Gitlab::UsageDataCounters::TrackUniqueEvents @@ -1014,6 +1017,20 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do counter.track_event(event_action: :created, event_target: project, author_id: 5, time: time - 3.days) counter.track_event(event_action: :created, event_target: wiki, author_id: 3) counter.track_event(event_action: :created, event_target: design, author_id: 3) + + counter = Gitlab::UsageDataCounters::EditorUniqueCounter + + counter.track_web_ide_edit_action(author: user1) + counter.track_web_ide_edit_action(author: user1) + counter.track_sfe_edit_action(author: user1) + counter.track_snippet_editor_edit_action(author: user1) + counter.track_snippet_editor_edit_action(author: user1, time: time - 3.days) + + counter.track_web_ide_edit_action(author: user2) + counter.track_sfe_edit_action(author: user2) + + counter.track_web_ide_edit_action(author: user3, time: time - 3.days) + counter.track_snippet_editor_edit_action(author: user3) end it 'returns the distinct count of user actions within the specified time period' do @@ -1021,7 +1038,11 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do { action_monthly_active_users_design_management: 1, action_monthly_active_users_project_repo: 3, - action_monthly_active_users_wiki_repo: 1 + action_monthly_active_users_wiki_repo: 1, + action_monthly_active_users_web_ide_edit: 2, + action_monthly_active_users_sfe_edit: 2, + action_monthly_active_users_snippet_editor_edit: 2, + action_monthly_active_users_ide_edit: 3 } ) end diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 3fb8708c884..334833e884b 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -3,25 +3,22 @@ require 'spec_helper' RSpec.describe CommitRange do + let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) } + let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) } + let(:full_sha_to) { commit2.id } + let(:full_sha_from) { commit1.id } + let(:sha_to) { commit2.short_id } + let(:sha_from) { commit1.short_id } + let!(:commit2) { project.commit } + let!(:commit1) { project.commit("HEAD~2") } + let!(:project) { create(:project, :public, :repository) } + describe 'modules' do subject { described_class } it { is_expected.to include_module(Referable) } end - let!(:project) { create(:project, :public, :repository) } - let!(:commit1) { project.commit("HEAD~2") } - let!(:commit2) { project.commit } - - let(:sha_from) { commit1.short_id } - let(:sha_to) { commit2.short_id } - - let(:full_sha_from) { commit1.id } - let(:full_sha_to) { commit2.id } - - let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) } - let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) } - it 'raises ArgumentError when given an invalid range string' do expect { described_class.new("Foo", project) }.to raise_error(ArgumentError) end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index b52b035e130..e611484f5ee 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -3,6 +3,11 @@ require 'spec_helper' RSpec.describe Milestone do + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project) } + let(:project) { create(:project, :public) } + it_behaves_like 'a timebox', :milestone describe 'MilestoneStruct#serializable_hash' do @@ -47,11 +52,6 @@ RSpec.describe Milestone do it { is_expected.to have_many(:milestone_releases) } end - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } - let(:issue) { create(:issue, project: project) } - let(:user) { create(:user) } - describe '.predefined_id?' do it 'returns true for a predefined Milestone ID' do expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb index f710385b6e2..35f282b638b 100644 --- a/spec/models/project_services/packagist_service_spec.rb +++ b/spec/models/project_services/packagist_service_spec.rb @@ -3,20 +3,6 @@ require 'spec_helper' RSpec.describe PackagistService do - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - let(:project) { create(:project) } - - let(:packagist_server) { 'https://packagist.example.com' } - let(:packagist_username) { 'theUser' } - let(:packagist_token) { 'verySecret' } - let(:packagist_hook_url) do - "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}" - end - let(:packagist_params) do { active: true, @@ -29,6 +15,20 @@ RSpec.describe PackagistService do } end + let(:packagist_hook_url) do + "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}" + end + + let(:packagist_token) { 'verySecret' } + let(:packagist_username) { 'theUser' } + let(:packagist_server) { 'https://packagist.example.com' } + let(:project) { create(:project) } + + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + describe '#execute' do let(:user) { create(:user) } let(:project) { create(:project, :repository) } diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index ae1abb50a40..3628171fcc1 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'get board lists' do nodes { lists { nodes { - issues { + issues(filters: {labelName: "#{label2.title}"}) { count nodes { #{all_graphql_fields_for('issues'.classify)} @@ -51,8 +51,8 @@ RSpec.describe 'get board lists' do shared_examples 'group and project board list issues query' do let!(:board) { create(:board, resource_parent: board_parent) } let!(:label_list) { create(:list, board: board, label: label, position: 10) } - let!(:issue1) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } - let!(:issue2) { create(:issue, project: issue_project, labels: [label], relative_position: 2) } + let!(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) } + let!(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) } let!(:issue3) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } let!(:issue4) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } @@ -72,7 +72,7 @@ RSpec.describe 'get board lists' do it 'can access the issues' do post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) - expect(issue_titles).to eq([issue2.title, issue3.title, issue1.title]) + expect(issue_titles).to eq([issue2.title, issue1.title]) end end end diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 9446e10c01d..22b003501a1 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'getting merge request listings nested in a project' do let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:current_user) { create(:user) } - let_it_be(:label) { create(:label) } + let_it_be(:label) { create(:label, project: project) } let_it_be(:merge_request_a) { create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) } let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) } let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) } diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index b0fbf3bf66d..3870c78deee 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -402,30 +402,76 @@ RSpec.describe API::Issues do expect_paginated_array_response([group_closed_issue.id, group_issue.id]) end - it 'returns an array of labeled group issues' do - get api(base_url, user), params: { labels: group_label.title } + shared_examples 'labels parameter' do + it 'returns an array of labeled group issues' do + get api(base_url, user), params: { labels: group_label.title } - expect_paginated_array_response(group_issue.id) - expect(json_response.first['labels']).to eq([group_label.title]) - end + expect_paginated_array_response(group_issue.id) + expect(json_response.first['labels']).to eq([group_label.title]) + end - it 'returns an array of labeled group issues with labels param as array' do - get api(base_url, user), params: { labels: [group_label.title] } + it 'returns an array of labeled group issues' do + get api(base_url, user), params: { labels: group_label.title } - expect_paginated_array_response(group_issue.id) - expect(json_response.first['labels']).to eq([group_label.title]) + expect_paginated_array_response(group_issue.id) + expect(json_response.first['labels']).to eq([group_label.title]) + end + + it 'returns an array of labeled group issues with labels param as array' do + get api(base_url, user), params: { labels: [group_label.title] } + + expect_paginated_array_response(group_issue.id) + expect(json_response.first['labels']).to eq([group_label.title]) + end + + it 'returns an array of labeled group issues where all labels match' do + get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" } + + expect_paginated_array_response([]) + end + + it 'returns an array of labeled group issues where all labels match with labels param as array' do + get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] } + + expect_paginated_array_response([]) + end + + context 'with labeled issues' do + let(:group_issue2) { create :issue, project: group_project } + let(:label_b) { create(:label, title: 'foo', project: group_project) } + let(:label_c) { create(:label, title: 'bar', project: group_project) } + + before do + create(:label_link, label: group_label, target: group_issue2) + create(:label_link, label: label_b, target: group_issue) + create(:label_link, label: label_b, target: group_issue2) + create(:label_link, label: label_c, target: group_issue) + + get api(base_url, user), params: params + end + + let(:issue) { group_issue } + let(:issue2) { group_issue2 } + let(:label) { group_label } + + it_behaves_like 'labeled issues with labels and label_name params' + end end - it 'returns an array of labeled group issues where all labels match' do - get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" } + context 'when `optimized_issuable_label_filter` feature flag is off' do + before do + stub_feature_flags(optimized_issuable_label_filter: false) + end - expect_paginated_array_response([]) + it_behaves_like 'labels parameter' end - it 'returns an array of labeled group issues where all labels match with labels param as array' do - get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] } + context 'when `optimized_issuable_label_filter` feature flag is on' do + before do + stub_feature_flags(optimized_issuable_label_filter: true) + end - expect_paginated_array_response([]) + it_behaves_like 'labels parameter' end it 'returns issues matching given search string for title' do @@ -440,27 +486,6 @@ RSpec.describe API::Issues do expect_paginated_array_response(group_issue.id) end - context 'with labeled issues' do - let(:group_issue2) { create :issue, project: group_project } - let(:label_b) { create(:label, title: 'foo', project: group_project) } - let(:label_c) { create(:label, title: 'bar', project: group_project) } - - before do - create(:label_link, label: group_label, target: group_issue2) - create(:label_link, label: label_b, target: group_issue) - create(:label_link, label: label_b, target: group_issue2) - create(:label_link, label: label_c, target: group_issue) - - get api(base_url, user), params: params - end - - let(:issue) { group_issue } - let(:issue2) { group_issue2 } - let(:label) { group_label } - - it_behaves_like 'labeled issues with labels and label_name params' - end - context 'with archived projects' do let_it_be(:archived_issue) do create( diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb index 5d96e8048bf..212d39caf74 100644 --- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb +++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb @@ -27,6 +27,8 @@ RSpec.describe RuboCop::Cop::Migration::UpdateColumnInBatches do FileUtils.rm_rf(tmp_rails_root) end + let(:spec_filepath) { tmp_rails_root.join('spec', 'migrations', 'my_super_migration_spec.rb') } + context 'outside of a migration' do it 'does not register any offenses' do inspect_source(migration_code) @@ -35,8 +37,6 @@ RSpec.describe RuboCop::Cop::Migration::UpdateColumnInBatches do end end - let(:spec_filepath) { tmp_rails_root.join('spec', 'migrations', 'my_super_migration_spec.rb') } - shared_context 'with a migration file' do before do FileUtils.mkdir_p(File.dirname(migration_filepath)) diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index 35ce7c7175c..1357836cb89 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -6,6 +6,10 @@ RSpec.describe PipelineDetailsEntity do let_it_be(:user) { create(:user) } let(:request) { double('request') } + let(:entity) do + described_class.represent(pipeline, request: request) + end + it 'inherrits from PipelineEntity' do expect(described_class).to be < PipelineEntity end @@ -16,10 +20,6 @@ RSpec.describe PipelineDetailsEntity do allow(request).to receive(:current_user).and_return(user) end - let(:entity) do - described_class.represent(pipeline, request: request) - end - describe '#as_json' do subject { entity.as_json } diff --git a/spec/services/ci/create_cross_project_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb index 1aabdb85afd..f26f9eeb6f0 100644 --- a/spec/services/ci/create_cross_project_pipeline_service_spec.rb +++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::CreateCrossProjectPipelineService, '#execute' do +RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do let_it_be(:user) { create(:user) } let(:upstream_project) { create(:project, :repository) } let_it_be(:downstream_project) { create(:project, :repository) } @@ -130,7 +130,7 @@ RSpec.describe Ci::CreateCrossProjectPipelineService, '#execute' do expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with( - instance_of(Ci::CreateCrossProjectPipelineService::DuplicateDownstreamPipelineError), + instance_of(described_class::DuplicateDownstreamPipelineError), bridge_id: bridge.id, project_id: bridge.project.id) .and_call_original expect(Ci::CreatePipelineService).not_to receive(:new) @@ -397,7 +397,7 @@ RSpec.describe Ci::CreateCrossProjectPipelineService, '#execute' do context 'when pipeline variables are defined' do before do - upstream_pipeline.variables.create(key: 'PIPELINE_VARIABLE', value: 'my-value') + upstream_pipeline.variables.create!(key: 'PIPELINE_VARIABLE', value: 'my-value') end it 'does not pass pipeline variables directly downstream' do diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb index ff86d44f5e8..7e5a225f020 100644 --- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb +++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb @@ -1,29 +1,47 @@ # frozen_string_literal: true -RSpec.shared_examples 'tracking unique hll events' do |method| - it 'tracks unique event if the format is HTML' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(instance_of(String), target_id) +RSpec.shared_examples 'tracking unique hll events' do |feature_flag| + context 'when format is HTML' do + let(:format) { :html } - get method, params: request_params, format: :html - end + it 'tracks unique event' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id) - it 'tracks unique event if DNT is not enabled' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(instance_of(String), target_id) - request.headers['DNT'] = '0' + subject + end - get method, params: request_params, format: :html - end + it 'tracks unique event if DNT is not enabled' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id) + request.headers['DNT'] = '0' + + subject + end + + it 'does not track unique event if DNT is enabled' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id) + request.headers['DNT'] = '1' - it 'does not track unique event if DNT is enabled' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(instance_of(String), target_id) - request.headers['DNT'] = '1' + subject + end - get method, params: request_params, format: :html + context 'when feature flag is disabled' do + it 'does not track unique event' do + stub_feature_flags(feature_flag => false) + + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id) + + subject + end + end end - it 'does not track unique event if the format is JSON' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(instance_of(String), target_id) + context 'when format is JSON' do + let(:format) { :json } + + it 'does not track unique event if the format is JSON' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id) - get method, params: request_params, format: :json + subject + end end end diff --git a/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb b/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb index 95dcf5624cc..116e6878281 100644 --- a/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb +++ b/spec/workers/ci/create_cross_project_pipeline_worker_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Ci::CreateCrossProjectPipelineWorker do describe '#perform' do context 'when bridge exists' do it 'calls cross project pipeline creation service' do - expect(Ci::CreateCrossProjectPipelineService) + expect(Ci::CreateDownstreamPipelineService) .to receive(:new) .with(project, user) .and_return(service) @@ -26,7 +26,7 @@ RSpec.describe Ci::CreateCrossProjectPipelineWorker do context 'when bridge does not exist' do it 'does nothing' do - expect(Ci::CreateCrossProjectPipelineService) + expect(Ci::CreateDownstreamPipelineService) .not_to receive(:new) described_class.new.perform(non_existing_record_id) |