diff options
33 files changed, 553 insertions, 233 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 342561afd0c..7a493ac2f09 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -4a6eb3bfb6ecd8324a39089cb3b59e282936331c +6db6349ef26137d13a7176897bcb01232b289521 diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue index c66b595ffdc..a5f8f369604 100644 --- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue +++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue @@ -1,10 +1,15 @@ <script> -import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf, GlBadge } from '@gitlab/ui'; +import { GlModal, GlTabs, GlTab, GlSprintf, GlBadge, GlFilteredSearch } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue'; import { createAlert } from '~/alert'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; +import { + OPERATORS_IS, + TOKEN_TYPE_AUTHOR, +} from '~/vue_shared/components/filtered_search_bar/constants'; +import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue'; import eventHub from '../event_hub'; import { findCommitIndex, @@ -12,6 +17,7 @@ import { removeIfReadyToBeRemoved, removeIfPresent, } from '../utils'; +import Token from './token.vue'; export default { components: { @@ -19,9 +25,9 @@ export default { GlTabs, GlTab, ReviewTabContainer, - GlSearchBoxByType, GlSprintf, GlBadge, + GlFilteredSearch, }, props: { contextCommitsPath: { @@ -41,6 +47,49 @@ export default { required: true, }, }, + data() { + return { + availableTokens: [ + { + icon: 'pencil', + title: __('Author'), + type: TOKEN_TYPE_AUTHOR, + operators: OPERATORS_IS, + token: UserToken, + defaultAuthors: [], + unique: true, + fetchAuthors: this.fetchAuthors, + initialAuthors: [], + }, + { + formattedKey: __('Committed-before'), + key: 'committed-before', + type: 'committed-before-date', + param: '', + symbol: '', + icon: 'clock', + tag: 'committed_before', + title: __('Committed-before'), + operators: OPERATORS_IS, + token: Token, + unique: true, + }, + { + formattedKey: __('Committed-after'), + key: 'committed-after', + type: 'committed-after-date', + param: '', + symbol: '', + icon: 'clock', + tag: 'committed_after', + title: __('Committed-after'), + operators: OPERATORS_IS, + token: Token, + unique: true, + }, + ], + }; + }, computed: { ...mapState([ 'tabIndex', @@ -98,8 +147,6 @@ export default { }, beforeDestroy() { eventHub.$off('openModal', this.openModal); - clearTimeout(this.timeout); - this.timeout = null; }, methods: { ...mapActions([ @@ -114,10 +161,8 @@ export default { 'setSearchText', 'setToRemoveCommits', 'resetModalState', + 'fetchAuthors', ]), - focusSearch() { - this.$refs.searchInput.focusInput(); - }, openModal() { this.searchCommits(); this.fetchContextCommits(); @@ -125,7 +170,6 @@ export default { }, handleTabChange(tabIndex) { if (tabIndex === 0) { - this.focusSearch(); if (this.shouldPurge) { this.setSelectedCommits( [...this.commits, ...this.selectedCommits].filter((commit) => commit.isSelected), @@ -133,17 +177,36 @@ export default { } } }, - handleSearchCommits(value) { - // We only call the service, if we have 3 characters or we don't have any characters - if (value.length >= 3) { - clearTimeout(this.timeout); - this.timeout = setTimeout(() => { - this.searchCommits(value); - }, 500); - } else if (value.length === 0) { - this.searchCommits(); + blurSearchInput() { + const searchInputEl = this.$refs.filteredSearchInput.$el.querySelector( + '.gl-filtered-search-token-segment-input', + ); + if (searchInputEl) { + searchInputEl.blur(); } - this.setSearchText(value); + }, + handleSearchCommits(value = []) { + const searchValues = value.reduce((acc, searchFilter) => { + const isEqualSearch = searchFilter?.value?.operator === '='; + + if (!isEqualSearch && typeof searchFilter === 'object') return acc; + + if (typeof searchFilter === 'string' && searchFilter.length >= 3) { + acc.searchText = searchFilter; + } else if (searchFilter?.type === 'author' && searchFilter?.value?.data?.length >= 3) { + acc.author = searchFilter?.value?.data; + } else if (searchFilter?.type === 'committed-before-date') { + acc.committed_before = searchFilter?.value?.data; + } else if (searchFilter?.type === 'committed-after-date') { + acc.committed_after = searchFilter?.value?.data; + } + + return acc; + }, {}); + + this.searchCommits(searchValues); + this.blurSearchInput(); + this.setSearchText(searchValues.searchText); }, handleCommitRowSelect(event) { const index = event[0]; @@ -208,11 +271,12 @@ export default { }, handleModalClose() { this.resetModalState(); - clearTimeout(this.timeout); }, handleModalHide() { this.resetModalState(); - clearTimeout(this.timeout); + }, + shouldShowInputDateFormat(value) { + return ['Committed-before', 'Committed-after'].indexOf(value) !== -1; }, }, }; @@ -223,13 +287,14 @@ export default { ref="modal" cancel-variant="light" size="md" + no-focus-on-show + modal-class="add-review-item-modal" body-class="add-review-item pt-0" :scrollable="true" :ok-title="__('Save changes')" modal-id="add-review-item" :title="__('Add or remove previously merged commits')" :ok-disabled="disableSaveButton" - @shown="focusSearch" @ok="handleCreateContextCommits" @cancel="handleModalClose" @close="handleModalClose" @@ -245,11 +310,24 @@ export default { </gl-sprintf> </template> <div class="gl-mt-3"> - <gl-search-box-by-type - ref="searchInput" - :placeholder="__(`Search by commit title or SHA`)" - @input="handleSearchCommits" - /> + <gl-filtered-search + ref="filteredSearchInput" + class="flex-grow-1" + :placeholder="__(`Search or filter commits`)" + :available-tokens="availableTokens" + @clear="handleSearchCommits" + @submit="handleSearchCommits" + > + <template #title="{ value }"> + <div> + {{ value }} + <span v-if="shouldShowInputDateFormat(value)" class="title-hint-text"> + <{{ __('yyyy-mm-dd') }}> + </span> + </div> + </template> + </gl-filtered-search> + <review-tab-container :is-loading="isLoadingCommits" :loading-error="commitsLoadingError" diff --git a/app/assets/javascripts/add_context_commits_modal/components/token.vue b/app/assets/javascripts/add_context_commits_modal/components/token.vue new file mode 100644 index 00000000000..c403adbbf60 --- /dev/null +++ b/app/assets/javascripts/add_context_commits_modal/components/token.vue @@ -0,0 +1,28 @@ +<script> +import { GlFilteredSearchToken } from '@gitlab/ui'; + +export default { + components: { + GlFilteredSearchToken, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + val: '', + }; + }, +}; +</script> + +<template> + <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners" /> +</template> diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js index de9c7488ace..f085b0d0e5e 100644 --- a/app/assets/javascripts/add_context_commits_modal/store/actions.js +++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js @@ -1,8 +1,11 @@ import _ from 'lodash'; +import * as Sentry from '@sentry/browser'; import Api from '~/api'; import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; +import { joinPaths } from '~/lib/utils/url_utility'; +import { ACTIVE_AND_BLOCKED_USER_STATES } from '~/users_select/constants'; import * as types from './mutation_types'; export const setBaseConfig = ({ commit }, options) => { @@ -11,14 +14,14 @@ export const setBaseConfig = ({ commit }, options) => { export const setTabIndex = ({ commit }, tabIndex) => commit(types.SET_TABINDEX, tabIndex); -export const searchCommits = ({ dispatch, commit, state }, searchText) => { +export const searchCommits = ({ dispatch, commit, state }, search = {}) => { commit(types.FETCH_COMMITS); let params = {}; - if (searchText) { + if (search) { params = { params: { - search: searchText, + ...search, per_page: 40, }, }; @@ -37,7 +40,7 @@ export const searchCommits = ({ dispatch, commit, state }, searchText) => { } return c; }); - if (!searchText) { + if (!search) { dispatch('setCommits', { commits: [...commits, ...state.contextCommits] }); } else { dispatch('setCommits', { commits }); @@ -131,6 +134,23 @@ export const setSelectedCommits = ({ commit }, selected) => { commit(types.SET_SELECTED_COMMITS, selectedCommits); }; +export const fetchAuthors = ({ dispatch, state }, author = null) => { + const { projectId } = state; + return axios + .get(joinPaths(gon.relative_url_root || '', '/-/autocomplete/users.json'), { + params: { + project_id: projectId, + states: ACTIVE_AND_BLOCKED_USER_STATES, + search: author, + }, + }) + .then(({ data }) => data) + .catch((error) => { + Sentry.captureException(error); + dispatch('receiveAuthorsError'); + }); +}; + export const setSearchText = ({ commit }, searchText) => commit(types.SET_SEARCH_TEXT, searchText); export const setToRemoveCommits = ({ commit }, data) => commit(types.SET_TO_REMOVE_COMMITS, data); diff --git a/app/assets/javascripts/add_context_commits_modal/store/index.js b/app/assets/javascripts/add_context_commits_modal/store/index.js index 0bf3441379b..560834a26ae 100644 --- a/app/assets/javascripts/add_context_commits_modal/store/index.js +++ b/app/assets/javascripts/add_context_commits_modal/store/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import Vuex from 'vuex'; +import filters from '~/vue_shared/components/filtered_search_bar/store/modules/filters'; import * as actions from './actions'; import mutations from './mutations'; import state from './state'; @@ -12,4 +13,5 @@ export default () => state: state(), actions, mutations, + modules: { filters }, }); diff --git a/app/assets/javascripts/add_context_commits_modal/store/state.js b/app/assets/javascripts/add_context_commits_modal/store/state.js index 37239adccbb..fed3148bc9e 100644 --- a/app/assets/javascripts/add_context_commits_modal/store/state.js +++ b/app/assets/javascripts/add_context_commits_modal/store/state.js @@ -1,4 +1,5 @@ export default () => ({ + projectId: '', contextCommitsPath: '', tabIndex: 0, isLoadingCommits: false, diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 2d5e9bc91f2..a2873622682 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -371,7 +371,7 @@ export function insertMarkdownText({ }); } -function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) { +export function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) { const $textArea = $(textArea); textArea = $textArea.get(0); const text = $textArea.val(); @@ -627,10 +627,9 @@ export function addMarkdownListeners(form) { }); const $allToolbarBtns = $(form) - .off('click', '.js-md, .saved-replies-dropdown li') - .on('click', '.js-md, .saved-replies-dropdown li', function () { - const $savedReplyContent = $('.js-saved-reply-content', this); - const $toolbarBtn = $savedReplyContent.length ? $savedReplyContent : $(this); + .off('click', '.js-md') + .on('click', '.js-md', function () { + const $toolbarBtn = $(this); return updateTextForToolbarBtn($toolbarBtn); }); diff --git a/app/assets/javascripts/vue_shared/components/markdown/saved_replies_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/saved_replies_dropdown.vue index 989b14f8711..b6581eed71a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/saved_replies_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/saved_replies_dropdown.vue @@ -1,6 +1,7 @@ <script> import { GlCollapsibleListbox, GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { updateText } from '~/lib/utils/text_markdown'; import savedRepliesQuery from './saved_replies.query.graphql'; export default { @@ -51,6 +52,24 @@ export default { setSavedRepliesSearch(search) { this.savedRepliesSearch = search; }, + onSelect(id) { + const savedReply = this.savedReplies.find((r) => r.id === id); + const textArea = this.$el.closest('.md-area')?.querySelector('textarea'); + + if (savedReply && textArea) { + updateText({ + textArea, + tag: savedReply.content, + cursorOffset: 0, + wrap: false, + }); + + // Wait for text to be added into textarea + requestAnimationFrame(() => { + textArea.focus(); + }); + } + }, }, }; </script> @@ -65,6 +84,7 @@ export default { :searching="$apollo.queries.savedReplies.loading" @shown="fetchSavedReplies" @search="setSavedRepliesSearch" + @select="onSelect" > <template #toggle> <gl-button @@ -74,19 +94,14 @@ export default { category="tertiary" class="gl-px-3!" data-testid="saved-replies-dropdown-toggle" + @keydown.prevent > <gl-icon name="symlink" class="gl-mr-0!" /> <gl-icon name="chevron-down" /> </gl-button> </template> <template #list-item="{ item }"> - <div - class="gl-display-flex js-saved-reply-content" - :data-md-tag="item.content" - data-md-cursor-offset="0" - data-md-prepend="true" - data-testid="saved-reply-dropdown-item" - > + <div class="gl-display-flex js-saved-reply-content"> <div class="gl-text-truncate"> <strong>{{ item.text }}</strong ><span class="gl-ml-2">{{ item.content }}</span> diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 225c32c1989..83f51588f43 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -332,3 +332,18 @@ height: 100%; } } + +.add-review-item-modal { + .modal-content { + position: absolute; + top: 5%; + } + + .title-hint-text { + color: $gl-text-color-secondary; + } + + .gl-filtered-search-suggestion-list.dropdown-menu { + width: $gl-max-dropdown-max-height; + } +} diff --git a/app/finders/context_commits_finder.rb b/app/finders/context_commits_finder.rb index 4a45817cc61..a186ca92c7b 100644 --- a/app/finders/context_commits_finder.rb +++ b/app/finders/context_commits_finder.rb @@ -21,20 +21,24 @@ class ContextCommitsFinder attr_reader :project, :merge_request, :search, :author, :committed_before, :committed_after, :limit def init_collection - if search.present? + if search_params_present? search_commits else project.repository.commits(merge_request.target_branch, { limit: limit }) end end + def search_params_present? + [search, author, committed_before, committed_after].map(&:present?).any? + end + def filter_existing_commits(commits) commits.select! { |commit| already_included_ids.exclude?(commit.id) } commits end def search_commits - key = search.strip + key = search&.strip commits = [] if Commit.valid_hash?(key) mr_existing_commits_ids = merge_request.commits.map(&:id) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 71434931d8c..56c8ec4cea3 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -26,6 +26,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord # rather than the persisted value. ADDRESSABLE_URL_VALIDATION_OPTIONS = { deny_all_requests_except_allowed: ->(settings) { settings.deny_all_requests_except_allowed } }.freeze + HUMANIZED_ATTRIBUTES = { + archive_builds_in_seconds: 'Archive job value' + }.freeze + enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }, _prefix: true @@ -336,7 +340,11 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord validates :archive_builds_in_seconds, allow_nil: true, - numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } + numericality: { + only_integer: true, + greater_than_or_equal_to: 1.day.seconds, + message: N_('must be at least 1 day') + } validates :local_markdown_version, allow_nil: true, @@ -873,6 +881,10 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord private + def self.human_attribute_name(attribute, *options) + HUMANIZED_ATTRIBUTES[attribute.to_sym] || super + end + def parsed_grafana_url @parsed_grafana_url ||= Gitlab::Utils.parse_url(grafana_url) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 587b71315c2..b4a0eaf0324 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -199,7 +199,7 @@ class Repository def list_commits_by(query, ref, author: nil, before: nil, after: nil, limit: 1000) return [] unless exists? return [] unless has_visible_content? - return [] unless query.present? && ref.present? + return [] unless ref.present? commits = raw_repository.list_commits_by( query, ref, author: author, before: before, after: after, limit: limit).map do |c| diff --git a/app/services/security/ci_configuration/base_create_service.rb b/app/services/security/ci_configuration/base_create_service.rb index 0534925aaec..b60a949fd4e 100644 --- a/app/services/security/ci_configuration/base_create_service.rb +++ b/app/services/security/ci_configuration/base_create_service.rb @@ -59,7 +59,8 @@ module Security YAML.safe_load(@gitlab_ci_yml) if @gitlab_ci_yml rescue Psych::BadAlias raise Gitlab::Graphql::Errors::MutationError, - ".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually." + Gitlab::Utils::ErrorMessage.to_user_facing( + _(".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually.")) rescue Psych::Exception => e Gitlab::AppLogger.error("Failed to process existing .gitlab-ci.yml: #{e.message}") raise Gitlab::Graphql::Errors::MutationError, diff --git a/config/feature_flags/development/use_marker_ranges.yml b/config/feature_flags/development/use_marker_ranges.yml deleted file mode 100644 index 068e403e2cf..00000000000 --- a/config/feature_flags/development/use_marker_ranges.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: use_marker_ranges -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56361 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324638 -milestone: '13.10' -type: development -group: group::source code -default_enabled: false diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index e276d3b25af..c113d1aeff4 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -632,6 +632,42 @@ To update this limit to a new value on a self-managed installation, run the foll Plan.default.actual_limits.update!(ci_instance_level_variables: 30) ``` +### Number of group level variables + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362227) in GitLab 15.7. + +The total number of group level CI/CD variables is limited at the instance level. +This limit is checked each time a new group level variable is created. If a new variable +would cause the total number of variables to exceed the limit, the new variable is not created. + +On self-managed instances this limit is defined for the `default` plan. By default, +this limit is set to `30000`. + +To update this limit to a new value on a self-managed installation, run the following in the +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): + +```ruby +Plan.default.actual_limits.update!(group_ci_variables: 40000) +``` + +### Number of project level variables + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362227) in GitLab 15.7. + +The total number of project level CI/CD variables is limited at the instance level. +This limit is checked each time a new project level variable is created. If a new variable +would cause the total number of variables to exceed the limit, the new variable is not created. + +On self-managed instances this limit is defined for the `default` plan. By default, +this limit is set to `8000`. + +To update this limit to a new value on a self-managed installation, run the following in the +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): + +```ruby +Plan.default.actual_limits.update!(project_ci_variables: 10000) +``` + ### Maximum file size per type of artifact > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37226) in GitLab 13.3. diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 225b4f7cf86..95ea3fe9f0f 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -24,15 +24,15 @@ module Gitlab end def highlight - populate_marker_ranges if Feature.enabled?(:use_marker_ranges, project) + populate_marker_ranges - @diff_lines.map.with_index do |diff_line, index| + @diff_lines.map do |diff_line| diff_line = diff_line.dup # ignore highlighting for "match" lines next diff_line if diff_line.meta? rich_line = apply_syntax_highlight(diff_line) - rich_line = apply_marker_ranges_highlight(diff_line, rich_line, index) + rich_line = apply_marker_ranges_highlight(diff_line, rich_line) diff_line.rich_text = rich_line @@ -60,12 +60,8 @@ module Gitlab highlight_line(diff_line) || ERB::Util.html_escape(diff_line.text) end - def apply_marker_ranges_highlight(diff_line, rich_line, index) - marker_ranges = if Feature.enabled?(:use_marker_ranges, project) - diff_line.marker_ranges - else - inline_diffs[index] - end + def apply_marker_ranges_highlight(diff_line, rich_line) + marker_ranges = diff_line.marker_ranges return rich_line if marker_ranges.blank? @@ -134,12 +130,6 @@ module Gitlab end end - # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/324638 - # ------------------------------------------------------------------------ - def inline_diffs - @inline_diffs ||= InlineDiff.for_lines(@raw_lines) - end - def old_lines @old_lines ||= highlighted_blob_lines(diff_file.old_blob) end diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb index 5128b09aef4..63a437b021d 100644 --- a/lib/gitlab/diff/highlight_cache.rb +++ b/lib/gitlab/diff/highlight_cache.rb @@ -71,7 +71,6 @@ module Gitlab strong_memoize(:redis_key) do options = [ diff_options, - Feature.enabled?(:use_marker_ranges, diffable.project), Feature.enabled?(:diff_line_syntax_highlighting, diffable.project) ] options_for_key = OpenSSL::Digest::SHA256.hexdigest(options.join) diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index 802da50cfc6..7f760a23f45 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -17,27 +17,6 @@ module Gitlab CharDiff.new(old_line, new_line).changed_ranges(offset: offset) end - - # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/324638 - class << self - def for_lines(lines) - pair_selector = Gitlab::Diff::PairSelector.new(lines) - - inline_diffs = [] - - pair_selector.each do |old_index, new_index| - old_line = lines[old_index] - new_line = lines[new_index] - - old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs - - inline_diffs[old_index] = old_diffs - inline_diffs[new_index] = new_diffs - end - - inline_diffs - end - end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ead55c0a94a..8f16eef3a4b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1478,6 +1478,9 @@ msgstr "" msgid "." msgstr "" +msgid ".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually." +msgstr "" + msgid "/" msgstr "" @@ -8999,7 +9002,16 @@ msgstr "" msgid "CiCatalog|CI/CD catalog" msgstr "" -msgid "CiCatalog|Repositories of reusable pipeline components available in this namespace." +msgid "CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier." +msgstr "" + +msgid "CiCatalog|Get started with the CI/CD catalog" +msgstr "" + +msgid "CiCatalog|Learn more" +msgstr "" + +msgid "CiCatalog|Repositories of pipeline components available in this namespace." msgstr "" msgid "CiCdAnalytics|Date range: %{range}" @@ -10562,6 +10574,12 @@ msgstr "" msgid "Committed by" msgstr "" +msgid "Committed-after" +msgstr "" + +msgid "Committed-before" +msgstr "" + msgid "CommonJS module" msgstr "" @@ -35450,6 +35468,9 @@ msgstr "" msgid "ProtectedEnvironment|Unprotect" msgstr "" +msgid "ProtectedEnvironment|Users with at least the Developer role can write to unprotected environments. Are you sure you want to unprotect %{environment_name}?" +msgstr "" + msgid "ProtectedEnvironment|Your environment can't be unprotected" msgstr "" @@ -38551,9 +38572,6 @@ msgstr "" msgid "Search by author" msgstr "" -msgid "Search by commit title or SHA" -msgstr "" - msgid "Search by message" msgstr "" @@ -38599,6 +38617,9 @@ msgstr "" msgid "Search or create tag" msgstr "" +msgid "Search or filter commits" +msgstr "" + msgid "Search or filter results..." msgstr "" @@ -40615,6 +40636,9 @@ msgstr "" msgid "Show latest version" msgstr "" +msgid "Show less" +msgstr "" + msgid "Show list" msgstr "" @@ -52385,6 +52409,9 @@ msgstr "" msgid "must be associated with a Group or a Project" msgstr "" +msgid "must be at least 1 day" +msgstr "" + msgid "must be greater than start date" msgstr "" @@ -52991,6 +53018,9 @@ msgstr "" msgid "your settings" msgstr "" +msgid "yyyy-mm-dd" +msgstr "" + msgid "{group}" msgstr "" diff --git a/scripts/pipeline/create_test_failure_issues.rb b/scripts/pipeline/create_test_failure_issues.rb index 6312d392760..efe86984fc9 100755 --- a/scripts/pipeline/create_test_failure_issues.rb +++ b/scripts/pipeline/create_test_failure_issues.rb @@ -24,7 +24,7 @@ class CreateTestFailureIssues puts "[CreateTestFailureIssues] No failed tests!" if failed_tests.empty? failed_tests.each_with_object([]) do |failed_test, existing_issues| - CreateTestFailureIssue.new(options.dup).comment_or_create(failed_test, existing_issues).tap do |issue| + CreateTestFailureIssue.new(options.dup).upsert(failed_test, existing_issues).tap do |issue| existing_issues << issue File.write(File.join(options[:issue_json_folder], "issue-#{issue.iid}.json"), JSON.pretty_generate(issue.to_h)) end @@ -52,14 +52,18 @@ class CreateTestFailureIssue WWW_GITLAB_COM_GROUPS_JSON = "#{WWW_GITLAB_COM_SITE}/groups.json".freeze WWW_GITLAB_COM_CATEGORIES_JSON = "#{WWW_GITLAB_COM_SITE}/categories.json".freeze FEATURE_CATEGORY_METADATA_REGEX = /(?<=feature_category: :)\w+/ - DEFAULT_LABELS = ['type::maintenance', 'failure::flaky-test'].freeze + DEFAULT_LABELS = ['type::maintenance', 'test'].freeze + PROJECT_PATH = ENV.fetch('CI_PROJECT_PATH', 'gitlab-org/gitlab') + JOB_BASE_URL = "https://gitlab.com/#{PROJECT_PATH}/-/jobs/".freeze + FILE_BASE_URL = "https://gitlab.com/#{PROJECT_PATH}/-/blob/master/".freeze + REPORT_ITEM_REGEX = %r{^1\. \d{4}-\d{2}-\d{2}: #{JOB_BASE_URL}.+$} def initialize(options) @project = options.delete(:project) @api_token = options.delete(:api_token) end - def comment_or_create(failed_test, existing_issues = []) + def upsert(failed_test, existing_issues = []) existing_issue = find(failed_test, existing_issues) if existing_issue @@ -70,12 +74,16 @@ class CreateTestFailureIssue end end + private + + attr_reader :project, :api_token + def find(failed_test, existing_issues = []) - failed_test_issue_title = failed_test_issue_title(failed_test) - issue_from_existing_issues = existing_issues.find { |issue| issue.title == failed_test_issue_title } + test_id = failed_test_id(failed_test) + issue_from_existing_issues = existing_issues.find { |issue| issue.title.include?(test_id) } issue_from_issue_tracker = FindIssues .new(project: project, api_token: api_token) - .execute(state: 'opened', search: failed_test_issue_title) + .execute(state: :opened, search: test_id, in: :title, per_page: 1) .first existing_issue = issue_from_existing_issues || issue_from_issue_tracker @@ -88,10 +96,24 @@ class CreateTestFailureIssue end def update_reports(existing_issue, failed_test) - new_issue_description = "#{existing_issue.description}\n- #{failed_test['job_url']} (#{ENV['CI_PIPELINE_URL']})" + # We count the number of existing reports. + reports_count = existing_issue.description + .scan(REPORT_ITEM_REGEX) + .size.to_i + 1 + + # We include the number of reports in the header, for visibility. + issue_description = existing_issue.description.sub(/^### Reports.*$/, "### Reports (#{reports_count})") + + # We add the current failure to the list of reports. + issue_description = "#{issue_description}\n#{report_list_item(failed_test)}" + UpdateIssue .new(project: project, api_token: api_token) - .execute(existing_issue.iid, description: new_issue_description) + .execute( + existing_issue.iid, + description: issue_description, + weight: reports_count + ) puts "[CreateTestFailureIssue] Added a report in '#{existing_issue.title}': #{existing_issue.web_url}!" end @@ -99,7 +121,8 @@ class CreateTestFailureIssue payload = { title: failed_test_issue_title(failed_test), description: failed_test_issue_description(failed_test), - labels: failed_test_issue_labels(failed_test) + labels: failed_test_issue_labels(failed_test), + weight: 1 } CreateIssue.new(project: project, api_token: api_token).execute(payload).tap do |issue| @@ -107,36 +130,40 @@ class CreateTestFailureIssue end end - private - - attr_reader :project, :api_token - def failed_test_id(failed_test) - Digest::SHA256.hexdigest(search_safe(failed_test['name']))[0...12] + Digest::SHA256.hexdigest(failed_test['file'] + failed_test['name'])[0...12] end def failed_test_issue_title(failed_test) - title = "#{failed_test['file']} - ID: #{failed_test_id(failed_test)}" + title = "#{failed_test['file']} [test-hash:#{failed_test_id(failed_test)}]" raise "Title is too long!" if title.size > MAX_TITLE_LENGTH title end + def test_file_link(failed_test) + "[`#{failed_test['file']}`](#{FILE_BASE_URL}#{failed_test['file']})" + end + + def report_list_item(failed_test) + "1. #{Time.new.utc.strftime('%F')}: #{failed_test['job_url']} (#{ENV['CI_PIPELINE_URL']})" + end + def failed_test_issue_description(failed_test) <<~DESCRIPTION - ### Full description + ### Test description `#{search_safe(failed_test['name'])}` - ### File path + ### Test file path - `#{failed_test['file']}` + #{test_file_link(failed_test)} <!-- Don't add anything after the report list since it's updated automatically --> - ### Reports + ### Reports (1) - - #{failed_test['job_url']} (#{ENV['CI_PIPELINE_URL']}) + #{report_list_item(failed_test)} DESCRIPTION end diff --git a/spec/features/profiles/user_uses_saved_reply_spec.rb b/spec/features/profiles/user_uses_saved_reply_spec.rb index f9a4f4a7fa6..4954a8ce67c 100644 --- a/spec/features/profiles/user_uses_saved_reply_spec.rb +++ b/spec/features/profiles/user_uses_saved_reply_spec.rb @@ -22,7 +22,7 @@ RSpec.describe 'User uses saved reply', :js, wait_for_requests - find('[data-testid="saved-reply-dropdown-item"]').click + find('.gl-new-dropdown-item').click expect(find('.note-textarea').value).to eq(saved_reply.content) end diff --git a/spec/finders/context_commits_finder_spec.rb b/spec/finders/context_commits_finder_spec.rb index c22675bc67d..3de1d29b695 100644 --- a/spec/finders/context_commits_finder_spec.rb +++ b/spec/finders/context_commits_finder_spec.rb @@ -26,27 +26,30 @@ RSpec.describe ContextCommitsFinder do end it 'returns commits based in author filter' do - params = { search: 'test text', author: 'Job van der Voort' } + params = { author: 'Job van der Voort' } commits = described_class.new(project, merge_request, params).execute expect(commits.length).to eq(1) expect(commits[0].id).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') end - it 'returns commits based in before filter' do - params = { search: 'test text', committed_before: 1474828200 } + it 'returns commits based in committed before and after filter' do + params = { committed_before: 1471631400, committed_after: 1471458600 } # August 18, 2016 - # August 20, 2016 commits = described_class.new(project, merge_request, params).execute - expect(commits.length).to eq(1) - expect(commits[0].id).to eq('498214de67004b1da3d820901307bed2a68a8ef6') + expect(commits.length).to eq(2) + expect(commits[0].id).to eq('1b12f15a11fc6e62177bef08f47bc7b5ce50b141') + expect(commits[1].id).to eq('38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e') end - it 'returns commits based in after filter' do - params = { search: 'test text', committed_after: 1474828200 } - commits = described_class.new(project, merge_request, params).execute + it 'returns commits from target branch if no filter is applied' do + expect(project.repository).to receive(:commits).with(merge_request.target_branch, anything).and_call_original - expect(commits.length).to eq(1) + commits = described_class.new(project, merge_request).execute + + expect(commits.length).to eq(37) expect(commits[0].id).to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0') + expect(commits[1].id).to eq('498214de67004b1da3d820901307bed2a68a8ef6') end end end diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap index 2c2151bfb41..e379aba094c 100644 --- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap +++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap @@ -6,8 +6,9 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = ` body-class="add-review-item pt-0" cancel-variant="light" dismisslabel="Close" - modalclass="" + modalclass="add-review-item-modal" modalid="add-review-item" + nofocusonshow="true" ok-disabled="true" ok-title="Save changes" scrollable="true" @@ -27,9 +28,13 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = ` <div class="gl-mt-3" > - <gl-search-box-by-type-stub + <gl-filtered-search-stub + availabletokens="[object Object],[object Object],[object Object]" + class="flex-grow-1" clearbuttontitle="Clear" - placeholder="Search by commit title or SHA" + placeholder="Search or filter commits" + searchbuttonattributes="[object Object]" + searchinputattributes="[object Object]" value="" /> diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js index 5e96da9af7e..27fe010c354 100644 --- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js +++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js @@ -1,4 +1,4 @@ -import { GlModal, GlSearchBoxByType } from '@gitlab/ui'; +import { GlModal, GlFilteredSearch } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; @@ -49,7 +49,7 @@ describe('AddContextCommitsModal', () => { }; const findModal = () => wrapper.findComponent(GlModal); - const findSearch = () => wrapper.findComponent(GlSearchBoxByType); + const findSearch = () => wrapper.findComponent(GlFilteredSearch); beforeEach(() => { wrapper = createWrapper(); @@ -68,12 +68,29 @@ describe('AddContextCommitsModal', () => { expect(findSearch().exists()).toBe(true); }); - it('when user starts entering text in search box, it calls action "searchCommits" after waiting for 500s', () => { - const searchText = 'abcd'; - findSearch().vm.$emit('input', searchText); - expect(searchCommits).not.toHaveBeenCalled(); - jest.advanceTimersByTime(500); - expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText); + it('when user submits after entering filters in search box, then it calls action "searchCommits"', () => { + const search = [ + 'abcd', + { + type: 'author', + value: { operator: '=', data: 'abhi' }, + }, + { + type: 'committed-before-date', + value: { operator: '=', data: '2022-10-31' }, + }, + { + type: 'committed-after-date', + value: { operator: '=', data: '2022-10-28' }, + }, + ]; + findSearch().vm.$emit('submit', search); + expect(searchCommits).toHaveBeenCalledWith(expect.anything(), { + searchText: 'abcd', + author: 'abhi', + committed_before: '2022-10-31', + committed_after: '2022-10-28', + }); }); it('disabled ok button when no row is selected', () => { diff --git a/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js index 8ad9ad30c1d..1b1c30f7739 100644 --- a/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/markdown/saved_replies_dropdown_spec.js @@ -4,9 +4,13 @@ import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_repl import { mountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import { updateText } from '~/lib/utils/text_markdown'; import SavedRepliesDropdown from '~/vue_shared/components/markdown/saved_replies_dropdown.vue'; import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql'; +jest.mock('~/lib/utils/text_markdown'); + let wrapper; let savedRepliesResp; @@ -24,6 +28,7 @@ function createComponent(options = {}) { const { mockApollo } = options; return mountExtended(SavedRepliesDropdown, { + attachTo: '#root', propsData: { newSavedRepliesPath: '/new', }, @@ -32,6 +37,14 @@ function createComponent(options = {}) { } describe('Saved replies dropdown', () => { + beforeEach(() => { + setHTMLFixture('<div class="md-area"><textarea></textarea><div id="root"></div></div>'); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + it('fetches data when dropdown gets opened', async () => { const mockApollo = createMockApolloProvider(savedRepliesResponse); wrapper = createComponent({ mockApollo }); @@ -43,7 +56,7 @@ describe('Saved replies dropdown', () => { expect(savedRepliesResp).toHaveBeenCalled(); }); - it('adds markdown toolbar attributes to dropdown items', async () => { + it('adds content to textarea', async () => { const mockApollo = createMockApolloProvider(savedRepliesResponse); wrapper = createComponent({ mockApollo }); @@ -51,12 +64,13 @@ describe('Saved replies dropdown', () => { await waitForPromises(); - expect(wrapper.findByTestId('saved-reply-dropdown-item').attributes()).toEqual( - expect.objectContaining({ - 'data-md-cursor-offset': '0', - 'data-md-prepend': 'true', - 'data-md-tag': 'Saved Reply Content', - }), - ); + wrapper.find('.gl-new-dropdown-item').trigger('click'); + + expect(updateText).toHaveBeenCalledWith({ + textArea: document.querySelector('textarea'), + tag: savedRepliesResponse.data.currentUser.savedReplies.nodes[0].content, + cursorOffset: 0, + wrap: false, + }); }); }); diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index 33e9360ee01..43e4f28b4df 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do +RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache, feature_category: :source_code_management do let_it_be(:merge_request) { create(:merge_request_with_diffs) } let(:diff_hash) do @@ -282,17 +282,7 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do end it 'returns cache key' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, true, true])}") - end - - context 'when the `use_marker_ranges` feature flag is disabled' do - before do - stub_feature_flags(use_marker_ranges: false) - end - - it 'returns the original version of the cache' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, false, true])}") - end + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, true])}") end context 'when the `diff_line_syntax_highlighting` feature flag is disabled' do @@ -301,7 +291,7 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do end it 'returns the original version of the cache' do - is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, true, false])}") + is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{options_hash([cache.diff_options, false])}") end end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index c378ecb8134..233dddbdad7 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Diff::Highlight do +RSpec.describe Gitlab::Diff::Highlight, feature_category: :source_code_management do include RepoHelpers let_it_be(:project) { create(:project, :repository) } @@ -15,7 +15,6 @@ RSpec.describe Gitlab::Diff::Highlight do let(:code) { '<h2 onmouseover="alert(2)">Test</h2>' } before do - allow(Gitlab::Diff::InlineDiff).to receive(:for_lines).and_return([]) allow_any_instance_of(Gitlab::Diff::Line).to receive(:text).and_return(code) end @@ -121,18 +120,6 @@ RSpec.describe Gitlab::Diff::Highlight do end end - context 'when `use_marker_ranges` feature flag is disabled' do - it 'returns the same result' do - with_feature_flag = described_class.new(diff_file, repository: project.repository).highlight - - stub_feature_flags(use_marker_ranges: false) - - without_feature_flag = described_class.new(diff_file, repository: project.repository).highlight - - expect(with_feature_flag.map(&:rich_text)).to eq(without_feature_flag.map(&:rich_text)) - end - end - context 'when no inline diffs' do it_behaves_like 'without inline diffs' end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 8387f4021b6..72e900125c6 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -534,6 +534,13 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do .is_less_than(65536) end + specify do + is_expected.to validate_numericality_of(:archive_builds_in_seconds) + .only_integer + .is_greater_than_or_equal_to(1.day.seconds.to_i) + .with_message('must be at least 1 day') + end + describe 'usage_ping_enabled setting' do shared_examples 'usage ping enabled' do it do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f970e818db9..72011693e20 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -597,6 +597,15 @@ RSpec.describe Repository, feature_category: :source_code_management do end describe '#list_commits_by' do + it 'returns commits when no filter is applied' do + commit_ids = repository.list_commits_by(nil, 'master', limit: 2).map(&:id) + + expect(commit_ids).to include( + 'b83d6e391c22777fca1ed3012fce84f633d7fed0', + '498214de67004b1da3d820901307bed2a68a8ef6' + ) + end + it 'returns commits with messages containing a given string' do commit_ids = repository.list_commits_by('test text', 'master').map(&:id) diff --git a/spec/scripts/pipeline/create_test_failure_issues_spec.rb b/spec/scripts/pipeline/create_test_failure_issues_spec.rb index fa27727542e..e8849c32255 100644 --- a/spec/scripts/pipeline/create_test_failure_issues_spec.rb +++ b/spec/scripts/pipeline/create_test_failure_issues_spec.rb @@ -3,16 +3,19 @@ # rubocop:disable RSpec/VerifiedDoubles require 'fast_spec_helper' +require 'active_support/testing/time_helpers' require 'rspec-parameterized' require_relative '../../../scripts/pipeline/create_test_failure_issues' RSpec.describe CreateTestFailureIssues, feature_category: :tooling do describe CreateTestFailureIssue do + include ActiveSupport::Testing::TimeHelpers + let(:env) do { - 'CI_JOB_URL' => 'ci_job_url', - 'CI_PIPELINE_URL' => 'ci_pipeline_url' + 'CI_JOB_URL' => 'https://gitlab.com/gitlab-org/gitlab/-/jobs/1234', + 'CI_PIPELINE_URL' => 'https://gitlab.com/gitlab-org/gitlab/-/pipelines/5678' } end @@ -36,7 +39,7 @@ RSpec.describe CreateTestFailureIssues, feature_category: :tooling do { 'name' => test_name, 'file' => test_file, - 'job_url' => 'job_url' + 'job_url' => env['CI_JOB_URL'] } end @@ -57,87 +60,143 @@ RSpec.describe CreateTestFailureIssues, feature_category: :tooling do } end + let(:test_id) { Digest::SHA256.hexdigest(failed_test['file'] + failed_test['name'])[0...12] } + let(:latest_format_issue_title) { "#{failed_test['file']} [test-hash:#{test_id}]" } + + around do |example| + freeze_time { example.run } + end + before do stub_env(env) + allow(creator).to receive(:puts) end - describe '#find' do - let(:expected_payload) do + describe '#upsert' do + let(:expected_search_payload) do { - state: 'opened', - search: "#{failed_test['file']} - ID: #{Digest::SHA256.hexdigest(failed_test['name'])[0...12]}" + state: :opened, + search: test_id, + in: :title, + per_page: 1 } end let(:find_issue_stub) { double('FindIssues') } - let(:issue_stub) { double(title: expected_payload[:title], web_url: 'issue_web_url') } + let(:issue_stub) { double('Issue', title: latest_format_issue_title, web_url: 'issue_web_url') } before do - allow(creator).to receive(:puts) + allow(File).to receive(:open).and_call_original + allow(File).to receive(:open).with(File.expand_path(File.join('..', '..', '..', test_file), __dir__)) + .and_return(test_file_stub) + allow(creator).to receive(:categories_mapping).and_return(categories_mapping) + allow(creator).to receive(:groups_mapping).and_return(groups_mapping) end - it 'calls FindIssues#execute(payload)' do - expect(FindIssues).to receive(:new).with(project: project, api_token: api_token).and_return(find_issue_stub) - expect(find_issue_stub).to receive(:execute).with(expected_payload).and_return([issue_stub]) + context 'when no issues are found' do + let(:expected_description) do + <<~DESCRIPTION + ### Test description - creator.find(failed_test) - end + `#{failed_test['name']}` - context 'when no issues are found' do - it 'calls FindIssues#execute(payload)' do - expect(FindIssues).to receive(:new).with(project: project, api_token: api_token).and_return(find_issue_stub) - expect(find_issue_stub).to receive(:execute).with(expected_payload).and_return([]) + ### Test file path + + [`#{failed_test['file']}`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/#{failed_test['file']}) + + <!-- Don't add anything after the report list since it's updated automatically --> + ### Reports (1) - creator.find(failed_test) + 1. #{Time.new.utc.strftime('%F')}: #{failed_test['job_url']} (#{env['CI_PIPELINE_URL']}) + DESCRIPTION + end + + let(:create_issue_stub) { double('CreateIssue') } + + let(:expected_create_payload) do + { + title: latest_format_issue_title, + description: expected_description, + labels: described_class::DEFAULT_LABELS.map { |label| "wip-#{label}" } + [ + "wip-#{categories_mapping['source_code_management']['label']}", + "wip-#{groups_mapping['source_code']['label']}" + ], + weight: 1 + } + end + + before do + allow(FindIssues).to receive(:new).with(project: project, api_token: api_token).and_return(find_issue_stub) + allow(find_issue_stub).to receive(:execute).with(expected_search_payload).and_return([]) + end + + it 'calls CreateIssue#execute(payload)' do + expect(CreateIssue).to receive(:new).with(project: project, api_token: api_token) + .and_return(create_issue_stub) + expect(create_issue_stub).to receive(:execute).with(expected_create_payload).and_return(issue_stub) + + creator.upsert(failed_test) end end - end - describe '#create' do - let(:expected_description) do - <<~DESCRIPTION - ### Full description + context 'when issues are found' do + let(:failed_test_report_line) do + "1. #{Time.new.utc.strftime('%F')}: #{failed_test['job_url']} (#{env['CI_PIPELINE_URL']})" + end - `#{failed_test['name']}` + let(:latest_format_issue_description) do + <<~DESCRIPTION + ### Test description - ### File path + `#{failed_test['name']}` - `#{failed_test['file']}` + ### Test file path - <!-- Don't add anything after the report list since it's updated automatically --> - ### Reports + [`#{failed_test['file']}`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/#{failed_test['file']}) - - #{failed_test['job_url']} (#{env['CI_PIPELINE_URL']}) - DESCRIPTION - end + <!-- Don't add anything after the report list since it's updated automatically --> + ### Reports (1) - let(:expected_payload) do - { - title: "#{failed_test['file']} - ID: #{Digest::SHA256.hexdigest(failed_test['name'])[0...12]}", - description: expected_description, - labels: described_class::DEFAULT_LABELS.map { |label| "wip-#{label}" } + [ - "wip-#{categories_mapping['source_code_management']['label']}", "wip-#{groups_mapping['source_code']['label']}" # rubocop:disable Layout/LineLength - ] - } - end + #{failed_test_report_line} + DESCRIPTION + end - let(:create_issue_stub) { double('CreateIssue') } - let(:issue_stub) { double(title: expected_payload[:title], web_url: 'issue_web_url') } + let(:issue_stub) do + double('Issue', iid: 42, title: issue_title, description: issue_description, web_url: 'issue_web_url') + end - before do - allow(creator).to receive(:puts) - allow(File).to receive(:open).and_call_original - allow(File).to receive(:open).with(File.expand_path(File.join('..', '..', '..', test_file), __dir__)) - .and_return(test_file_stub) - allow(creator).to receive(:categories_mapping).and_return(categories_mapping) - allow(creator).to receive(:groups_mapping).and_return(groups_mapping) - end + let(:update_issue_stub) { double('UpdateIssue') } - it 'calls CreateIssue#execute(payload)' do - expect(CreateIssue).to receive(:new).with(project: project, api_token: api_token).and_return(create_issue_stub) - expect(create_issue_stub).to receive(:execute).with(expected_payload).and_return(issue_stub) + let(:expected_update_payload) do + { + description: latest_format_issue_description.sub(/^### Reports.*$/, '### Reports (2)') + + "\n#{failed_test_report_line}", + weight: 2 + } + end + + before do + allow(FindIssues).to receive(:new).with(project: project, api_token: api_token).and_return(find_issue_stub) + allow(find_issue_stub).to receive(:execute).with(expected_search_payload).and_return([issue_stub]) + end - creator.create(failed_test) # rubocop:disable Rails/SaveBang + # This shared example can be useful if we want to test migration to a new format in the future + shared_examples 'existing issue update' do + it 'calls UpdateIssue#execute(payload)' do + expect(UpdateIssue).to receive(:new).with(project: project, api_token: api_token) + .and_return(update_issue_stub) + expect(update_issue_stub).to receive(:execute).with(42, **expected_update_payload).and_return(issue_stub) + + creator.upsert(failed_test) + end + end + + context 'when issue already has the latest format' do + let(:issue_description) { latest_format_issue_description } + let(:issue_title) { latest_format_issue_title } + + it_behaves_like 'existing issue update' + end end end end diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb index 9fcdd296ebe..094c91f2ab5 100644 --- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb @@ -114,7 +114,8 @@ RSpec.shared_examples_for 'services security ci configuration create service' do it 'fails with error' do expect(project).to receive(:ci_config_for).and_return(unsupported_yaml) - expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, '.gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually.') + expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, Gitlab::Utils::ErrorMessage.to_user_facing( + _(".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually."))) end end diff --git a/workhorse/go.mod b/workhorse/go.mod index 6403e68b4da..25107ca4338 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -7,7 +7,7 @@ require ( github.com/BurntSushi/toml v1.2.1 github.com/FZambia/sentinel v1.1.1 github.com/alecthomas/chroma/v2 v2.6.0 - github.com/aws/aws-sdk-go v1.44.218 + github.com/aws/aws-sdk-go v1.44.224 github.com/disintegration/imaging v1.6.2 github.com/getsentry/raven-go v0.2.0 github.com/golang-jwt/jwt/v4 v4.5.0 diff --git a/workhorse/go.sum b/workhorse/go.sum index 8b87771139c..ec9949436a3 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -569,8 +569,8 @@ github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4 github.com/aws/aws-sdk-go v1.44.156/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.187/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.200/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go v1.44.218 h1:p707+xOCazWhkSpZOeyhtTcg7Z+asxxvueGgYPSitn4= -github.com/aws/aws-sdk-go v1.44.218/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.224 h1:09CiaaF35nRmxrzWZ2uRq5v6Ghg/d2RiPjZnSgtt+RQ= +github.com/aws/aws-sdk-go v1.44.224/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2 v1.17.4 h1:wyC6p9Yfq6V2y98wfDsj6OnNQa4w2BLGCLIxzNhwOGY= github.com/aws/aws-sdk-go-v2 v1.17.4/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= |