diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-11 09:13:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-11 09:13:09 +0300 |
commit | be7d70b884e6fa66c52862f38bf0f39b0631868b (patch) | |
tree | 235616671718bf2f39855f663677b61a55a8d68c | |
parent | 848ba57883b4ea9164bcb56a16c0fcb2b55b56e6 (diff) |
Add latest changes from gitlab-org/gitlab@master
60 files changed, 1368 insertions, 70 deletions
@@ -341,7 +341,7 @@ group :development do gem 'lefthook', '~> 0.7.0', require: false gem 'solargraph', '~> 0.43', require: false - gem 'letter_opener_web', '~> 1.4.0' + gem 'letter_opener_web', '~> 1.4.1' # Better errors handler gem 'better_errors', '~> 2.9.0' diff --git a/Gemfile.lock b/Gemfile.lock index dbed17712f7..1fc0e3b7139 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -699,7 +699,7 @@ GEM lefthook (0.7.5) letter_opener (1.7.0) launchy (~> 2.2) - letter_opener_web (1.4.0) + letter_opener_web (1.4.1) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -1512,7 +1512,7 @@ DEPENDENCIES kramdown (~> 2.3.1) kubeclient (~> 4.9.2) lefthook (~> 0.7.0) - letter_opener_web (~> 1.4.0) + letter_opener_web (~> 1.4.1) license_finder (~> 6.0) licensee (~> 9.14.1) lockbox (~> 0.6.2) diff --git a/README.md b/README.md index ee7eef9aa2d..73d0ffc3d34 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL/OpenSUSE - Ruby (MRI) 2.7.4 -- Git 2.31+ +- Git 2.33+ - Redis 5.0+ - PostgreSQL 12+ diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index ebf2ab0381e..b27dccabdf8 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -306,6 +306,12 @@ export const GO_TO_PROJECT_WIKI = { defaultKeys: ['g w'], // eslint-disable-line @gitlab/require-i18n-strings }; +export const GO_TO_PROJECT_WEBIDE = { + id: 'project.goToWebIDE', + description: __('Open in Web IDE'), + defaultKeys: ['.'], +}; + export const PROJECT_FILES_MOVE_SELECTION_UP = { id: 'projectFiles.moveSelectionUp', description: __('Move selection up'), @@ -549,6 +555,7 @@ export const PROJECT_SHORTCUTS_GROUP = { GO_TO_PROJECT_KUBERNETES, GO_TO_PROJECT_SNIPPETS, GO_TO_PROJECT_WIKI, + GO_TO_PROJECT_WEBIDE, ], }; diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js index b188d3b0ec3..7d8e4dd490c 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js @@ -1,4 +1,5 @@ import Mousetrap from 'mousetrap'; +import { visitUrl, constructWebIDEPath } from '~/lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; import { keysFor, @@ -18,6 +19,7 @@ import { GO_TO_PROJECT_KUBERNETES, GO_TO_PROJECT_ENVIRONMENTS, GO_TO_PROJECT_METRICS, + GO_TO_PROJECT_WEBIDE, NEW_ISSUE, } from './keybindings'; import Shortcuts from './shortcuts'; @@ -58,6 +60,18 @@ export default class ShortcutsNavigation extends Shortcuts { findAndFollowLink('.shortcuts-environments'), ); Mousetrap.bind(keysFor(GO_TO_PROJECT_METRICS), () => findAndFollowLink('.shortcuts-metrics')); + Mousetrap.bind(keysFor(GO_TO_PROJECT_WEBIDE), ShortcutsNavigation.navigateToWebIDE); Mousetrap.bind(keysFor(NEW_ISSUE), () => findAndFollowLink('.shortcuts-new-issue')); } + + static navigateToWebIDE() { + const path = constructWebIDEPath({ + sourceProjectFullPath: window.gl.mrWidgetData?.source_project_full_path, + targetProjectFullPath: window.gl.mrWidgetData?.target_project_full_path, + iid: window.gl.mrWidgetData?.iid, + }); + if (path) { + visitUrl(path); + } + } } diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 6580a028e4a..1c22d21a313 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -590,3 +590,30 @@ export function isSameOriginUrl(url) { return false; } } + +/** + * Returns a URL to WebIDE considering the current user's position in + * repository's tree. If not MR `iid` has been passed, the URL is fetched + * from the global `gl.webIDEPath`. + * + * @param sourceProjectFullPath Source project's full path. Used in MRs + * @param targetProjectFullPath Target project's full path. Used in MRs + * @param iid MR iid + * @returns {string} + */ + +export function constructWebIDEPath({ + sourceProjectFullPath, + targetProjectFullPath = '', + iid, +} = {}) { + if (!iid || !sourceProjectFullPath) { + return window.gl?.webIDEPath; + } + return mergeUrlParams( + { + target_project: sourceProjectFullPath !== targetProjectFullPath ? targetProjectFullPath : '', + }, + webIDEUrl(`/${sourceProjectFullPath}/merge_requests/${iid}`), + ); +} diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index db84e2b5912..d3717f10ec7 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -9,11 +9,13 @@ import { } from '@gitlab/ui'; import permissionsQuery from 'shared_queries/repository/permissions.query.graphql'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __ } from '../../locale'; import getRefMixin from '../mixins/get_ref'; import projectPathQuery from '../queries/project_path.query.graphql'; import projectShortPathQuery from '../queries/project_short_path.query.graphql'; import UploadBlobModal from './upload_blob_modal.vue'; +import NewDirectoryModal from './new_directory_modal.vue'; const ROW_TYPES = { header: 'header', @@ -21,6 +23,7 @@ const ROW_TYPES = { }; const UPLOAD_BLOB_MODAL_ID = 'modal-upload-blob'; +const NEW_DIRECTORY_MODAL_ID = 'modal-new-directory'; export default { components: { @@ -30,6 +33,7 @@ export default { GlDropdownItem, GlIcon, UploadBlobModal, + NewDirectoryModal, }, apollo: { projectShortPath: { @@ -54,7 +58,7 @@ export default { directives: { GlModal: GlModalDirective, }, - mixins: [getRefMixin], + mixins: [getRefMixin, glFeatureFlagsMixin()], props: { currentPath: { type: String, @@ -121,8 +125,14 @@ export default { required: false, default: '', }, + newDirPath: { + type: String, + required: false, + default: '', + }, }, uploadBlobModalId: UPLOAD_BLOB_MODAL_ID, + newDirectoryModalId: NEW_DIRECTORY_MODAL_ID, data() { return { projectShortPath: '', @@ -160,6 +170,13 @@ export default { showUploadModal() { return this.canEditTree && !this.$apollo.queries.userPermissions.loading; }, + showNewDirectoryModal() { + return ( + this.glFeatures.newDirModal && + this.canEditTree && + !this.$apollo.queries.userPermissions.loading + ); + }, dropdownItems() { const items = []; @@ -185,15 +202,26 @@ export default { text: __('Upload file'), modalId: UPLOAD_BLOB_MODAL_ID, }, - { + ); + + if (this.glFeatures.newDirModal) { + items.push({ + attrs: { + href: '#modal-create-new-dir', + }, + text: __('New directory'), + modalId: NEW_DIRECTORY_MODAL_ID, + }); + } else { + items.push({ attrs: { href: '#modal-create-new-dir', 'data-target': '#modal-create-new-dir', 'data-toggle': 'modal', }, text: __('New directory'), - }, - ); + }); + } } else if (this.canCreateMrFromFork) { items.push( { @@ -306,5 +334,14 @@ export default { :can-push-code="canPushCode" :path="uploadPath" /> + <new-directory-modal + v-if="showNewDirectoryModal" + :can-push-code="canPushCode" + :modal-id="$options.newDirectoryModalId" + :commit-message="__('Add new directory')" + :target-branch="selectedBranch" + :original-branch="originalBranch" + :path="newDirPath" + /> </nav> </template> diff --git a/app/assets/javascripts/repository/components/new_directory_modal.vue b/app/assets/javascripts/repository/components/new_directory_modal.vue new file mode 100644 index 00000000000..6c5797bf5b2 --- /dev/null +++ b/app/assets/javascripts/repository/components/new_directory_modal.vue @@ -0,0 +1,183 @@ +<script> +import { + GlAlert, + GlForm, + GlModal, + GlFormGroup, + GlFormInput, + GlFormTextarea, + GlToggle, +} from '@gitlab/ui'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; +import { + SECONDARY_OPTIONS_TEXT, + COMMIT_LABEL, + TARGET_BRANCH_LABEL, + TOGGLE_CREATE_MR_LABEL, + NEW_BRANCH_IN_FORK, +} from '../constants'; + +const MODAL_TITLE = __('Create New Directory'); +const PRIMARY_OPTIONS_TEXT = __('Create directory'); +const DIR_LABEL = __('Directory name'); +const ERROR_MESSAGE = __('Error creating new directory. Please try again.'); + +export default { + components: { + GlAlert, + GlModal, + GlForm, + GlFormGroup, + GlFormInput, + GlFormTextarea, + GlToggle, + }, + i18n: { + DIR_LABEL, + COMMIT_LABEL, + TARGET_BRANCH_LABEL, + TOGGLE_CREATE_MR_LABEL, + NEW_BRANCH_IN_FORK, + PRIMARY_OPTIONS_TEXT, + ERROR_MESSAGE, + }, + props: { + modalTitle: { + type: String, + default: MODAL_TITLE, + required: false, + }, + modalId: { + type: String, + required: true, + }, + primaryBtnText: { + type: String, + default: PRIMARY_OPTIONS_TEXT, + required: false, + }, + commitMessage: { + type: String, + required: true, + }, + targetBranch: { + type: String, + required: true, + }, + originalBranch: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + canPushCode: { + type: Boolean, + required: true, + }, + }, + data() { + return { + dir: null, + commit: this.commitMessage, + target: this.targetBranch, + createNewMr: true, + loading: false, + }; + }, + computed: { + primaryOptions() { + return { + text: this.primaryBtnText, + attributes: [ + { + variant: 'confirm', + loading: this.loading, + disabled: !this.formCompleted || this.loading, + }, + ], + }; + }, + cancelOptions() { + return { + text: SECONDARY_OPTIONS_TEXT, + attributes: [ + { + disabled: this.loading, + }, + ], + }; + }, + showCreateNewMrToggle() { + return this.canPushCode; + }, + formCompleted() { + return this.dir && this.commit && this.target; + }, + }, + methods: { + submitForm() { + this.loading = true; + + const formData = new FormData(); + formData.append('dir_name', this.dir); + formData.append('commit_message', this.commit); + formData.append('branch_name', this.target); + formData.append('original_branch', this.originalBranch); + + if (this.createNewMr) { + formData.append('create_merge_request', this.createNewMr); + } + + return axios + .post(this.path, formData) + .then((response) => { + visitUrl(response.data.filePath); + }) + .catch(() => { + this.loading = false; + createFlash({ message: ERROR_MESSAGE }); + }); + }, + }, +}; +</script> + +<template> + <gl-form> + <gl-modal + :modal-id="modalId" + :title="modalTitle" + :action-primary="primaryOptions" + :action-cancel="cancelOptions" + @primary.prevent="submitForm" + > + <gl-form-group :label="$options.i18n.DIR_LABEL" label-for="dir_name"> + <gl-form-input v-model="dir" :disabled="loading" name="dir_name" /> + </gl-form-group> + <gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message"> + <gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" /> + </gl-form-group> + <gl-form-group + v-if="canPushCode" + :label="$options.i18n.TARGET_BRANCH_LABEL" + label-for="branch_name" + > + <gl-form-input v-model="target" :disabled="loading" name="branch_name" /> + </gl-form-group> + <gl-toggle + v-if="showCreateNewMrToggle" + v-model="createNewMr" + :disabled="loading" + :label="$options.i18n.TOGGLE_CREATE_MR_LABEL" + /> + <gl-alert v-if="!canPushCode" variant="info" :dismissible="false" class="gl-mt-3"> + {{ $options.i18n.NEW_BRANCH_IN_FORK }} + </gl-alert> + </gl-modal> + </gl-form> +</template> diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js index 70952c8413b..152fabbd7cc 100644 --- a/app/assets/javascripts/repository/constants.js +++ b/app/assets/javascripts/repository/constants.js @@ -10,6 +10,9 @@ export const SECONDARY_OPTIONS_TEXT = __('Cancel'); export const COMMIT_LABEL = __('Commit message'); export const TARGET_BRANCH_LABEL = __('Target branch'); export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes'); +export const NEW_BRANCH_IN_FORK = __( + 'A new branch will be created in your fork and a new merge request will be started.', +); export const COMMIT_MESSAGE_SUBJECT_MAX_LENGTH = 52; export const COMMIT_MESSAGE_BODY_MAX_LENGTH = 72; diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index 60a1a0443f7..45e026ad695 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -120,6 +120,7 @@ export default function setupVueRepositoryList() { forkNewDirectoryPath, forkUploadBlobPath, uploadPath, + newDirPath, }, }); }, diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js index 6637d03a7a4..0a675e14eb5 100644 --- a/app/assets/javascripts/repository/router.js +++ b/app/assets/javascripts/repository/router.js @@ -1,7 +1,7 @@ import { escapeRegExp } from 'lodash'; import Vue from 'vue'; import VueRouter from 'vue-router'; -import { joinPaths } from '../lib/utils/url_utility'; +import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility'; import BlobPage from './pages/blob.vue'; import IndexPage from './pages/index.vue'; import TreePage from './pages/tree.vue'; @@ -24,7 +24,7 @@ export default function createRouter(base, baseRef) { }), }; - return new VueRouter({ + const router = new VueRouter({ mode: 'history', base: joinPaths(gon.relative_url_root || '', base), routes: [ @@ -59,4 +59,21 @@ export default function createRouter(base, baseRef) { }, ], }); + + router.afterEach((to) => { + const needsClosingSlash = !to.name.includes('blobPath'); + window.gl.webIDEPath = webIDEUrl( + joinPaths( + '/', + base, + 'edit', + decodeURI(baseRef), + '-', + to.params.path || '', + needsClosingSlash && '/', + ), + ); + }); + + return router; } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 966262944ad..ecabe5007e6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -10,7 +10,7 @@ import { GlSafeHtmlDirective as SafeHtml, GlSprintf, } from '@gitlab/ui'; -import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; +import { constructWebIDEPath } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; @@ -58,15 +58,7 @@ export default { }); }, webIdePath() { - return mergeUrlParams( - { - target_project: - this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath - ? this.mr.targetProjectFullPath - : '', - }, - webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`), - ); + return constructWebIDEPath(this.mr); }, isFork() { return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath; diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index cb0e1900e48..a76d45411dd 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -18,6 +18,7 @@ class Projects::TreeController < Projects::ApplicationController before_action do push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml) push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml) + push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml) end feature_category :source_code_management diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7c7e6457020..26da0436dd8 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -38,6 +38,7 @@ class ProjectsController < Projects::ApplicationController push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml) push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml) push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml) + push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml) end layout :determine_layout diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb index 0171473a77f..5a1e92efc96 100644 --- a/app/graphql/resolvers/project_pipelines_resolver.rb +++ b/app/graphql/resolvers/project_pipelines_resolver.rb @@ -26,3 +26,5 @@ module Resolvers end end # rubocop: enable Graphql/ResolverType + +Resolvers::ProjectPipelinesResolver.prepend_mod diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7b890a630cc..7cb821eab0b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1111,15 +1111,23 @@ class MergeRequest < ApplicationRecord can_be_merged? && !should_be_rebased? end + # rubocop: disable CodeReuse/ServiceClass def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) return false unless open? return false if work_in_progress? return false if broken? - return false unless skip_ci_check || mergeable_ci_state? return false unless skip_discussions_check || mergeable_discussions_state? - true + if Feature.enabled?(:improved_mergeability_checks, self.project, default_enabled: :yaml) + additional_checks = MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: { skip_ci_check: skip_ci_check }) + additional_checks.execute.all?(&:success?) + else + return false unless skip_ci_check || mergeable_ci_state? + + true + end end + # rubocop: enable CodeReuse/ServiceClass def ff_merge_possible? project.repository.ancestor?(target_branch_sha, diff_head_sha) diff --git a/app/services/merge_requests/mergeability/check_base_service.rb b/app/services/merge_requests/mergeability/check_base_service.rb new file mode 100644 index 00000000000..d5ddcb4b828 --- /dev/null +++ b/app/services/merge_requests/mergeability/check_base_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +module MergeRequests + module Mergeability + class CheckBaseService + attr_reader :merge_request, :params + + def initialize(merge_request:, params:) + @merge_request = merge_request + @params = params + end + + def skip? + raise NotImplementedError + end + + # When this method is true, we need to implement a cache_key + def cacheable? + raise NotImplementedError + end + + def cache_key + raise NotImplementedError + end + + private + + def success(*args) + Gitlab::MergeRequests::Mergeability::CheckResult.success(*args) + end + + def failure(*args) + Gitlab::MergeRequests::Mergeability::CheckResult.failed(*args) + end + end + end +end diff --git a/app/services/merge_requests/mergeability/check_ci_status_service.rb b/app/services/merge_requests/mergeability/check_ci_status_service.rb new file mode 100644 index 00000000000..c0ef5ba1c30 --- /dev/null +++ b/app/services/merge_requests/mergeability/check_ci_status_service.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +module MergeRequests + module Mergeability + class CheckCiStatusService < CheckBaseService + def execute + if merge_request.mergeable_ci_state? + success + else + failure + end + end + + def skip? + params[:skip_ci_check].present? + end + + def cacheable? + false + end + end + end +end diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb new file mode 100644 index 00000000000..c1d65fb65cc --- /dev/null +++ b/app/services/merge_requests/mergeability/run_checks_service.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +module MergeRequests + module Mergeability + class RunChecksService + include Gitlab::Utils::StrongMemoize + + # We want to have the cheapest checks first in the list, + # that way we can fail fast before running the more expensive ones + CHECKS = [ + CheckCiStatusService + ].freeze + + def initialize(merge_request:, params:) + @merge_request = merge_request + @params = params + end + + def execute + CHECKS.each_with_object([]) do |check_class, results| + check = check_class.new(merge_request: merge_request, params: params) + + next if check.skip? + + check_result = run_check(check) + results << check_result + + break results if check_result.failed? + end + end + + private + + attr_reader :merge_request, :params + + def run_check(check) + return check.execute unless Feature.enabled?(:mergeability_caching, merge_request.project, default_enabled: :yaml) + return check.execute unless check.cacheable? + + cached_result = results.read(merge_check: check) + return cached_result if cached_result.respond_to?(:status) + + check.execute.tap do |result| + results.write(merge_check: check, result_hash: result.to_hash) + end + end + + def results + strong_memoize(:results) do + Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request) + end + end + end + end +end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index af041de5596..c5395138902 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -248,7 +248,7 @@ module MergeRequests def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) - MergeRequests::MergeOrchestrationService + ::MergeRequests::MergeOrchestrationService .new(project, current_user, { sha: last_diff_sha }) .execute(merge_request) end diff --git a/app/views/layouts/_startup_js.html.haml b/app/views/layouts/_startup_js.html.haml index b7dd3a9556c..0d5f6bbe25b 100644 --- a/app/views/layouts/_startup_js.html.haml +++ b/app/views/layouts/_startup_js.html.haml @@ -25,6 +25,7 @@ headers: { "Content-Type": "application/json", ...headers, + } }; gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({ diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 597a22bf34a..cdcc98552f9 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -20,5 +20,6 @@ = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout), project_buttons: true #js-tree-list{ data: vue_file_list_data(project, ref) } - - if can_edit_tree? + - if !Feature.enabled?(:new_dir_modal, default_enabled: :yaml) && can_edit_tree? = render 'projects/blob/new_dir' + diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 66e9badbafb..168b240c657 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -18,3 +18,4 @@ = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put = render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration? += render 'shared/web_ide_path' diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml index 47a0d05fc65..459742c3b81 100644 --- a/app/views/projects/merge_requests/_widget.html.haml +++ b/app/views/projects/merge_requests/_widget.html.haml @@ -19,6 +19,6 @@ window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}'; window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}'; window.gl.mrWidgetData.false_positive_doc_url = '#{help_page_path('user/application_security/vulnerabilities/index')}'; - window.gl.mrWidgetData.can_view_false_positive = '#{(Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && @merge_request.project.licensed_feature_available?(:sast_fp_reduction)).to_s}'; + window.gl.mrWidgetData.can_view_false_positive = '#{@merge_request.project.licensed_feature_available?(:sast_fp_reduction).to_s}'; #js-vue-mr-widget.mr-widget diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index ff5582f2627..2154ef6b596 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -99,3 +99,4 @@ = render 'projects/invite_members_modal', project: @project - if Gitlab::CurrentSettings.gitpod_enabled && !current_user&.gitpod_enabled = render 'shared/gitpod/enable_gitpod_modal' += render 'shared/web_ide_path' diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 2d0c4cc20a0..1553eda1cfb 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -11,3 +11,4 @@ = render 'projects/last_push' = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) += render 'shared/web_ide_path' diff --git a/app/views/shared/_web_ide_path.html.haml b/app/views/shared/_web_ide_path.html.haml new file mode 100644 index 00000000000..73d00bcd408 --- /dev/null +++ b/app/views/shared/_web_ide_path.html.haml @@ -0,0 +1,4 @@ += javascript_tag do + :plain + window.gl = window.gl || {}; + window.gl.webIDEPath = '#{web_ide_url}' diff --git a/config/feature_flags/development/improved_mergeability_checks.yml b/config/feature_flags/development/improved_mergeability_checks.yml new file mode 100644 index 00000000000..83450ffa16f --- /dev/null +++ b/config/feature_flags/development/improved_mergeability_checks.yml @@ -0,0 +1,8 @@ +--- +name: improved_mergeability_checks +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68312 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342386 +milestone: '14.4' +type: development +group: group::code review +default_enabled: false diff --git a/config/feature_flags/development/mergeability_caching.yml b/config/feature_flags/development/mergeability_caching.yml new file mode 100644 index 00000000000..b9063299926 --- /dev/null +++ b/config/feature_flags/development/mergeability_caching.yml @@ -0,0 +1,8 @@ +--- +name: mergeability_caching +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68312 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340810 +milestone: '14.4' +type: development +group: group::code review +default_enabled: false diff --git a/config/feature_flags/development/vulnerability_flags.yml b/config/feature_flags/development/new_dir_modal.yml index 6ea7dd2e3f1..12d007209b7 100644 --- a/config/feature_flags/development/vulnerability_flags.yml +++ b/config/feature_flags/development/new_dir_modal.yml @@ -1,8 +1,8 @@ --- -name: vulnerability_flags -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66775 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340203 -milestone: '14.3' +name: new_dir_modal +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71154 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341675 +milestone: '14.4' type: development -group: group::static analysis +group: group::source code default_enabled: true diff --git a/db/post_migrate/20211006145004_finalize_indexes_for_ci_job_artifacts_expire_at_unlocked.rb b/db/post_migrate/20211006145004_finalize_indexes_for_ci_job_artifacts_expire_at_unlocked.rb new file mode 100644 index 00000000000..b046ab6ab03 --- /dev/null +++ b/db/post_migrate/20211006145004_finalize_indexes_for_ci_job_artifacts_expire_at_unlocked.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class FinalizeIndexesForCiJobArtifactsExpireAtUnlocked < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + TABLE_NAME = 'ci_job_artifacts' + INDEX_NAME = 'ci_job_artifacts_expire_at_unlocked_idx' + + def up + add_concurrent_index TABLE_NAME, [:expire_at], where: 'locked = 0', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME + end +end diff --git a/db/schema_migrations/20211006145004 b/db/schema_migrations/20211006145004 new file mode 100644 index 00000000000..6a99396d34a --- /dev/null +++ b/db/schema_migrations/20211006145004 @@ -0,0 +1 @@ +9fca672eaa0b82a37c211de35a4961b81fb163d290004907be7bf641327c65b1
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d49007990ee..6071ecdb82a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -24066,6 +24066,8 @@ CREATE INDEX cadence_create_iterations_automation ON iterations_cadences USING b CREATE INDEX ci_builds_gitlab_monitor_metrics ON ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text); +CREATE INDEX ci_job_artifacts_expire_at_unlocked_idx ON ci_job_artifacts USING btree (expire_at) WHERE (locked = 0); + CREATE INDEX code_owner_approval_required ON protected_branches USING btree (project_id, code_owner_approval_required) WHERE (code_owner_approval_required = true); CREATE UNIQUE INDEX commit_user_mentions_on_commit_id_and_note_id_unique_index ON commit_user_mentions USING btree (commit_id, note_id); diff --git a/doc/install/installation.md b/doc/install/installation.md index 7114a0a147c..852ddea41bd 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -51,7 +51,7 @@ If the highest number stable branch is unclear, check the [GitLab blog](https:// | -------- | --------------- | ----- | | [Ruby](#2-ruby) | `2.7` | From GitLab 13.6, Ruby 2.7 is required. Ruby 3.0 is not supported yet (see [the relevant epic](https://gitlab.com/groups/gitlab-org/-/epics/5149) for the current status). You must use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab needs several Gems that have native extensions. | | [Go](#3-go) | `1.15` | | -| [Git](#git) | `2.31.x` | From GitLab 13.11, Git 2.31.x and later is required. It's highly recommended that you use the [Git version provided by Gitaly](#git). | +| [Git](#git) | `2.33.x` | From GitLab 14.4, Git 2.33.x and later is required. It's highly recommended that you use the [Git version provided by Gitaly](#git). | | [Node.js](#4-node) | `12.22.1` | GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets. Node.js 14.x is recommended, as it's faster. You can check which version you're running with `node -v`. You need to update it to a newer version if needed. | ## GitLab directory structure diff --git a/doc/update/index.md b/doc/update/index.md index 057d2bbd831..b719c47ae26 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -307,6 +307,11 @@ NOTE: Specific information that follow related to Ruby and Git versions do not apply to [Omnibus installations](https://docs.gitlab.com/omnibus/) and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with appropriate Ruby and Git versions and are not using system binaries for Ruby and Git. There is no need to install Ruby or Git when utilizing these two approaches. +### 14.4.0 + +Git 2.33.x and later is required. We recommend you use the +[Git version provided by Gitaly](../install/installation.md#git). + ### 14.3.0 Ruby 2.7.4 is required. Refer to [the Ruby installation instructions](../install/installation.md#2-ruby) diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index abd00b3f555..993e5ee94dd 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -365,9 +365,6 @@ To create a custom ruleset: > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292686) in GitLab 14.2. -FLAG: -On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the `vulnerability_flags` flag](../../../administration/feature_flags.md). On GitLab.com, this feature is available. - Vulnerabilities that have been detected and are false positives will be flagged as false positives in the security dashboard. ### Using CI/CD variables to pass credentials for private repositories @@ -540,6 +537,12 @@ all [custom variables](../../../ci/variables/index.md#custom-cicd-variables) are to the underlying SAST analyzer images if [the SAST vendored template](#configuration) is used. +NOTE: +In [GitLab 13.3 and earlier](https://gitlab.com/gitlab-org/gitlab/-/issues/220540), +variables whose names started with the following prefixes are **not** propagated to either the +analyzer containers or SAST Docker container: `DOCKER_`, `CI`, `GITLAB_`, `FF_`, `HOME`, `PWD`, +`OLDPWD`, `PATH`, `SHLVL`, `HOSTNAME`. + ### Experimental features You can receive early access to experimental features. Experimental features might be added, diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb index df12aea1988..c723c2762c7 100644 --- a/lib/gitlab/email/handler/create_merge_request_handler.rb +++ b/lib/gitlab/email/handler/create_merge_request_handler.rb @@ -61,7 +61,7 @@ module Gitlab private def build_merge_request - MergeRequests::BuildService.new(project: project, current_user: author, params: merge_request_params).execute + ::MergeRequests::BuildService.new(project: project, current_user: author, params: merge_request_params).execute end def create_merge_request @@ -78,7 +78,7 @@ module Gitlab if merge_request.errors.any? merge_request else - MergeRequests::CreateService.new(project: project, current_user: author).create(merge_request) + ::MergeRequests::CreateService.new(project: project, current_user: author).create(merge_request) end end diff --git a/lib/gitlab/merge_requests/mergeability/check_result.rb b/lib/gitlab/merge_requests/mergeability/check_result.rb new file mode 100644 index 00000000000..d0788c7d7c7 --- /dev/null +++ b/lib/gitlab/merge_requests/mergeability/check_result.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +module Gitlab + module MergeRequests + module Mergeability + class CheckResult + SUCCESS_STATUS = :success + FAILED_STATUS = :failed + + attr_reader :status, :payload + + def self.default_payload + { last_run_at: Time.current } + end + + def self.success(payload: {}) + new(status: SUCCESS_STATUS, payload: default_payload.merge(payload)) + end + + def self.failed(payload: {}) + new(status: FAILED_STATUS, payload: default_payload.merge(payload)) + end + + def self.from_hash(data) + new( + status: data.fetch(:status), + payload: data.fetch(:payload)) + end + + def initialize(status:, payload: {}) + @status = status + @payload = payload + end + + def to_hash + { status: status, payload: payload } + end + + def failed? + status == FAILED_STATUS + end + + def success? + status == SUCCESS_STATUS + end + end + end + end +end diff --git a/lib/gitlab/merge_requests/mergeability/redis_interface.rb b/lib/gitlab/merge_requests/mergeability/redis_interface.rb new file mode 100644 index 00000000000..081ccfca360 --- /dev/null +++ b/lib/gitlab/merge_requests/mergeability/redis_interface.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +module Gitlab + module MergeRequests + module Mergeability + class RedisInterface + EXPIRATION = 6.hours + VERSION = 1 + + def save_check(merge_check:, result_hash:) + Gitlab::Redis::SharedState.with do |redis| + redis.set(merge_check.cache_key + ":#{VERSION}", result_hash.to_json, ex: EXPIRATION) + end + end + + def retrieve_check(merge_check:) + Gitlab::Redis::SharedState.with do |redis| + Gitlab::Json.parse(redis.get(merge_check.cache_key + ":#{VERSION}")) + end + end + end + end + end +end diff --git a/lib/gitlab/merge_requests/mergeability/results_store.rb b/lib/gitlab/merge_requests/mergeability/results_store.rb new file mode 100644 index 00000000000..bb6489f8526 --- /dev/null +++ b/lib/gitlab/merge_requests/mergeability/results_store.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +module Gitlab + module MergeRequests + module Mergeability + class ResultsStore + def initialize(interface: RedisInterface.new, merge_request:) + @interface = interface + @merge_request = merge_request + end + + def read(merge_check:) + interface.retrieve_check(merge_check: merge_check) + end + + def write(merge_check:, result_hash:) + interface.save_check(merge_check: merge_check, result_hash: result_hash) + end + + private + + attr_reader :interface + end + end + end +end diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb index 6348a4902f8..cc2021e14e3 100644 --- a/lib/gitlab/quick_actions/merge_request_actions.rb +++ b/lib/gitlab/quick_actions/merge_request_actions.rb @@ -148,7 +148,7 @@ module Gitlab quick_action_target.persisted? && quick_action_target.can_be_approved_by?(current_user) end command :approve do - success = MergeRequests::ApprovalService.new(project: quick_action_target.project, current_user: current_user).execute(quick_action_target) + success = ::MergeRequests::ApprovalService.new(project: quick_action_target.project, current_user: current_user).execute(quick_action_target) next unless success @@ -162,7 +162,7 @@ module Gitlab quick_action_target.persisted? && quick_action_target.can_be_unapproved_by?(current_user) end command :unapprove do - success = MergeRequests::RemoveApprovalService.new(project: quick_action_target.project, current_user: current_user).execute(quick_action_target) + success = ::MergeRequests::RemoveApprovalService.new(project: quick_action_target.project, current_user: current_user).execute(quick_action_target) next unless success @@ -275,7 +275,7 @@ module Gitlab end def merge_orchestration_service - @merge_orchestration_service ||= MergeRequests::MergeOrchestrationService.new(project, current_user) + @merge_orchestration_service ||= ::MergeRequests::MergeOrchestrationService.new(project, current_user) end def preferred_auto_merge_strategy(merge_request) diff --git a/lib/system_check/app/git_version_check.rb b/lib/system_check/app/git_version_check.rb index 31456dc096b..6512b142969 100644 --- a/lib/system_check/app/git_version_check.rb +++ b/lib/system_check/app/git_version_check.rb @@ -7,7 +7,7 @@ module SystemCheck set_check_pass -> { "yes (#{self.current_version})" } def self.required_version - @required_version ||= Gitlab::VersionInfo.parse('2.31.0') + @required_version ||= Gitlab::VersionInfo.parse('2.33.0') end def self.current_version diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7e38030b870..314bed47aa3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13272,6 +13272,9 @@ msgstr "" msgid "Error creating label." msgstr "" +msgid "Error creating new directory. Please try again." +msgstr "" + msgid "Error creating new iteration" msgstr "" @@ -23912,6 +23915,9 @@ msgstr "" msgid "Open errors" msgstr "" +msgid "Open in Web IDE" +msgstr "" + msgid "Open in file view" msgstr "" diff --git a/package.json b/package.json index 2c095f06e96..4d099102674 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "sql.js": "^0.4.0", "string-hash": "1.1.3", "style-loader": "^2.0.0", - "swagger-ui-dist": "^3.44.1", + "swagger-ui-dist": "^3.52.3", "three": "^0.84.0", "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb index 46b93d738e1..5ad7641a5be 100644 --- a/spec/features/projects/files/user_creates_directory_spec.rb +++ b/spec/features/projects/files/user_creates_directory_spec.rb @@ -98,12 +98,14 @@ RSpec.describe 'Projects > Files > User creates a directory', :js do expect(page).to have_content(fork_message) find('.add-to-tree').click + wait_for_requests click_link('New directory') fill_in(:dir_name, with: 'new_directory') fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Create directory') fork = user.fork_of(project2.reload) + wait_for_requests expect(current_path).to eq(project_new_merge_request_path(fork)) end diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 6f186ba3227..18b68d91e01 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -1004,4 +1004,39 @@ describe('URL utility', () => { expect(urlUtils.isSameOriginUrl(url)).toBe(expected); }); }); + + describe('constructWebIDEPath', () => { + let originalGl; + const projectIDEPath = '/foo/bar'; + const sourceProj = 'my_-fancy-proj/boo'; + const targetProj = 'boo/another-fancy-proj'; + const mrIid = '7'; + + beforeEach(() => { + originalGl = window.gl; + window.gl = { webIDEPath: projectIDEPath }; + }); + + afterEach(() => { + window.gl = originalGl; + }); + + it.each` + sourceProjectFullPath | targetProjectFullPath | iid | expectedPath + ${undefined} | ${undefined} | ${undefined} | ${projectIDEPath} + ${undefined} | ${undefined} | ${mrIid} | ${projectIDEPath} + ${undefined} | ${targetProj} | ${undefined} | ${projectIDEPath} + ${undefined} | ${targetProj} | ${mrIid} | ${projectIDEPath} + ${sourceProj} | ${undefined} | ${undefined} | ${projectIDEPath} + ${sourceProj} | ${targetProj} | ${undefined} | ${projectIDEPath} + ${sourceProj} | ${undefined} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`} + ${sourceProj} | ${sourceProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`} + ${sourceProj} | ${targetProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=${encodeURIComponent(targetProj)}`} + `( + 'returns $expectedPath for "$sourceProjectFullPath + $targetProjectFullPath + $iid"', + ({ expectedPath, ...args } = {}) => { + expect(urlUtils.constructWebIDEPath(args)).toBe(expectedPath); + }, + ); + }); }); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index 0733cffe4f4..eb957c635ac 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -2,6 +2,7 @@ import { GlDropdown } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; +import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; const defaultMockRoute = { name: 'blobPath', @@ -10,7 +11,7 @@ const defaultMockRoute = { describe('Repository breadcrumbs component', () => { let wrapper; - const factory = (currentPath, extraProps = {}, mockRoute = {}) => { + const factory = (currentPath, extraProps = {}, mockRoute = {}, newDirModal = true) => { const $apollo = { queries: { userPermissions: { @@ -34,10 +35,12 @@ describe('Repository breadcrumbs component', () => { }, $apollo, }, + provide: { glFeatures: { newDirModal } }, }); }; const findUploadBlobModal = () => wrapper.find(UploadBlobModal); + const findNewDirectoryModal = () => wrapper.find(NewDirectoryModal); afterEach(() => { wrapper.destroy(); @@ -121,4 +124,37 @@ describe('Repository breadcrumbs component', () => { expect(findUploadBlobModal().exists()).toBe(true); }); }); + + describe('renders the new directory modal', () => { + describe('with the feature flag enabled', () => { + beforeEach(() => { + window.gon.features = { + newDirModal: true, + }; + factory('/', { canEditTree: true }); + }); + + it('does not render the modal while loading', () => { + expect(findNewDirectoryModal().exists()).toBe(false); + }); + + it('renders the modal once loaded', async () => { + wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } }); + + await wrapper.vm.$nextTick(); + + expect(findNewDirectoryModal().exists()).toBe(true); + }); + }); + + describe('with the feature flag disabled', () => { + it('does not render the modal', () => { + window.gon.features = { + newDirModal: false, + }; + factory('/', { canEditTree: true }, {}, {}, false); + expect(findNewDirectoryModal().exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js new file mode 100644 index 00000000000..fe7f024e3ea --- /dev/null +++ b/spec/frontend/repository/components/new_directory_modal_spec.js @@ -0,0 +1,203 @@ +import { GlModal, GlFormTextarea, GlToggle } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { visitUrl } from '~/lib/utils/url_utility'; +import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; + +jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); + +const initialProps = { + modalTitle: 'Create New Directory', + modalId: 'modal-new-directory', + commitMessage: 'Add new directory', + targetBranch: 'some-target-branch', + originalBranch: 'master', + canPushCode: true, + path: 'create_dir', +}; + +const defaultFormValue = { + dirName: 'foo', + originalBranch: initialProps.originalBranch, + branchName: initialProps.targetBranch, + commitMessage: initialProps.commitMessage, + createNewMr: true, +}; + +describe('NewDirectoryModal', () => { + let wrapper; + let mock; + + const createComponent = (props = {}) => { + wrapper = shallowMount(NewDirectoryModal, { + propsData: { + ...initialProps, + ...props, + }, + attrs: { + static: true, + visible: true, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + const findDirName = () => wrapper.find('[name="dir_name"]'); + const findBranchName = () => wrapper.find('[name="branch_name"]'); + const findCommitMessage = () => wrapper.findComponent(GlFormTextarea); + const findMrToggle = () => wrapper.findComponent(GlToggle); + + const fillForm = async (inputValue = {}) => { + const { + dirName = defaultFormValue.dirName, + branchName = defaultFormValue.branchName, + commitMessage = defaultFormValue.commitMessage, + createNewMr = true, + } = inputValue; + + await findDirName().vm.$emit('input', dirName); + await findBranchName().vm.$emit('input', branchName); + await findCommitMessage().vm.$emit('input', commitMessage); + await findMrToggle().vm.$emit('change', createNewMr); + await nextTick; + }; + + const submitForm = async () => { + const mockEvent = { preventDefault: jest.fn() }; + findModal().vm.$emit('primary', mockEvent); + await waitForPromises(); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders modal component', () => { + createComponent(); + + const { modalTitle: title } = initialProps; + + expect(findModal().props()).toMatchObject({ + title, + size: 'md', + actionPrimary: { + text: NewDirectoryModal.i18n.PRIMARY_OPTIONS_TEXT, + }, + actionCancel: { + text: 'Cancel', + }, + }); + }); + + describe('form', () => { + it.each` + component | defaultValue | canPushCode | targetBranch | originalBranch | exist + ${findDirName} | ${undefined} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findBranchName} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findBranchName} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false} + ${findCommitMessage} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findMrToggle} | ${'true'} | ${true} | ${'new-target-branch'} | ${'master'} | ${true} + ${findMrToggle} | ${'true'} | ${true} | ${'master'} | ${'master'} | ${true} + `( + 'has the correct form fields ', + ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => { + createComponent({ + canPushCode, + targetBranch, + originalBranch, + }); + const formField = component(); + + if (!exist) { + expect(formField.exists()).toBe(false); + return; + } + + expect(formField.exists()).toBe(true); + expect(formField.attributes('value')).toBe(defaultValue); + }, + ); + }); + + describe('form submission', () => { + beforeEach(async () => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('valid form', () => { + beforeEach(() => { + createComponent(); + }); + + it('passes the formData', async () => { + const { + dirName, + branchName, + commitMessage, + originalBranch, + createNewMr, + } = defaultFormValue; + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + await fillForm(); + await submitForm(); + + expect(mock.history.post[0].data.get('dir_name')).toEqual(dirName); + expect(mock.history.post[0].data.get('branch_name')).toEqual(branchName); + expect(mock.history.post[0].data.get('commit_message')).toEqual(commitMessage); + expect(mock.history.post[0].data.get('original_branch')).toEqual(originalBranch); + expect(mock.history.post[0].data.get('create_merge_request')).toEqual(String(createNewMr)); + }); + + it('does not submit "create_merge_request" formData if createNewMr is not checked', async () => { + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + await fillForm({ createNewMr: false }); + await submitForm(); + expect(mock.history.post[0].data.get('create_merge_request')).toBeNull(); + }); + + it('redirects to the new directory', async () => { + const response = { filePath: 'new-dir-path' }; + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, response); + + await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); + await submitForm(); + + expect(visitUrl).toHaveBeenCalledWith(response.filePath); + }); + }); + + describe('invalid form', () => { + beforeEach(() => { + createComponent(); + }); + + it('disables submit button', async () => { + await fillForm({ dirName: '', branchName: '', commitMessage: '' }); + expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true); + }); + + it('creates a flash error', async () => { + mock.onPost(initialProps.path).timeout(); + + await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); + await submitForm(); + + expect(createFlash).toHaveBeenCalledWith({ + message: NewDirectoryModal.i18n.ERROR_MESSAGE, + }); + }); + }); + }); +}); diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index bb82fa706fd..3f822db601f 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -24,4 +24,32 @@ describe('Repository router spec', () => { expect(componentsForRoute).toContain(component); } }); + + describe('Storing Web IDE path globally', () => { + const proj = 'foo-bar-group/foo-bar-proj'; + let originalGl; + + beforeEach(() => { + originalGl = window.gl; + }); + + afterEach(() => { + window.gl = originalGl; + }); + + it.each` + path | branch | expectedPath + ${'/'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/tree/feat(test)'} | ${'feat(test)'} | ${`/-/ide/project/${proj}/edit/feat(test)/-/`} + ${'/-/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/-/tree/main/app/assets'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/app/assets/`} + ${'/-/blob/main/file.md'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/file.md`} + `('generates the correct Web IDE url for $path', ({ path, branch, expectedPath } = {}) => { + const router = createRouter(proj, branch); + + router.push(path); + expect(window.gl.webIDEPath).toBe(expectedPath); + }); + }); }); diff --git a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb index c7c00f54c0c..51a63e66b93 100644 --- a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb +++ b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb @@ -11,15 +11,23 @@ RSpec.describe Resolvers::ProjectPipelinesResolver do let(:current_user) { create(:user) } - before do - project.add_developer(current_user) + context 'when the user does have access' do + before do + project.add_developer(current_user) + end + + it 'resolves only MRs for the passed merge request' do + expect(resolve_pipelines).to contain_exactly(pipeline) + end end - def resolve_pipelines - resolve(described_class, obj: project, ctx: { current_user: current_user }) + context 'when the user does not have access' do + it 'does not return pipeline data' do + expect(resolve_pipelines).to be_empty + end end - it 'resolves only MRs for the passed merge request' do - expect(resolve_pipelines).to contain_exactly(pipeline) + def resolve_pipelines + resolve(described_class, obj: project, ctx: { current_user: current_user }) end end diff --git a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb new file mode 100644 index 00000000000..4f437e57600 --- /dev/null +++ b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do + subject(:check_result) { described_class } + + let(:time) { Time.current } + + around do |example| + freeze_time do + example.run + end + end + + describe '.default_payload' do + it 'returns the expected defaults' do + expect(check_result.default_payload).to eq({ last_run_at: time }) + end + end + + describe '.success' do + subject(:success) { check_result.success(payload: payload) } + + let(:payload) { {} } + + it 'creates a success result' do + expect(success.status).to eq described_class::SUCCESS_STATUS + end + + it 'uses the default payload' do + expect(success.payload).to eq described_class.default_payload + end + + context 'when given a payload' do + let(:payload) { { last_run_at: time + 1.day, test: 'test' } } + + it 'uses the payload passed' do + expect(success.payload).to eq payload + end + end + end + + describe '.failed' do + subject(:failed) { check_result.failed(payload: payload) } + + let(:payload) { {} } + + it 'creates a failure result' do + expect(failed.status).to eq described_class::FAILED_STATUS + end + + it 'uses the default payload' do + expect(failed.payload).to eq described_class.default_payload + end + + context 'when given a payload' do + let(:payload) { { last_run_at: time + 1.day, test: 'test' } } + + it 'uses the payload passed' do + expect(failed.payload).to eq payload + end + end + end + + describe '.from_hash' do + subject(:from_hash) { described_class.from_hash(hash) } + + let(:status) { described_class::SUCCESS_STATUS } + let(:payload) { { test: 'test' } } + let(:hash) do + { + status: status, + payload: payload + } + end + + it 'returns the expected status and payload' do + expect(from_hash.status).to eq status + expect(from_hash.payload).to eq payload + end + end + + describe '#to_hash' do + subject(:to_hash) { described_class.new(**hash).to_hash } + + let(:status) { described_class::SUCCESS_STATUS } + let(:payload) { { test: 'test' } } + let(:hash) do + { + status: status, + payload: payload + } + end + + it 'returns the expected hash' do + expect(to_hash).to eq hash + end + end + + describe '#failed?' do + subject(:failed) { described_class.new(status: status).failed? } + + context 'when it has failed' do + let(:status) { described_class::FAILED_STATUS } + + it 'returns true' do + expect(failed).to eq true + end + end + + context 'when it has succeeded' do + let(:status) { described_class::SUCCESS_STATUS } + + it 'returns false' do + expect(failed).to eq false + end + end + end + + describe '#success?' do + subject(:success) { described_class.new(status: status).success? } + + context 'when it has failed' do + let(:status) { described_class::FAILED_STATUS } + + it 'returns false' do + expect(success).to eq false + end + end + + context 'when it has succeeded' do + let(:status) { described_class::SUCCESS_STATUS } + + it 'returns true' do + expect(success).to eq true + end + end + end +end diff --git a/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb new file mode 100644 index 00000000000..e5475d04d86 --- /dev/null +++ b/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::MergeRequests::Mergeability::RedisInterface, :clean_gitlab_redis_shared_state do + subject(:redis_interface) { described_class.new } + + let(:merge_check) { double(cache_key: '13') } + let(:result_hash) { { 'test' => 'test' } } + let(:expected_key) { "#{merge_check.cache_key}:#{described_class::VERSION}" } + + describe '#save_check' do + it 'saves the hash' do + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(expected_key) }).to be_nil + + redis_interface.save_check(merge_check: merge_check, result_hash: result_hash) + + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(expected_key) }).to eq result_hash.to_json + end + end + + describe '#retrieve_check' do + it 'returns the hash' do + Gitlab::Redis::SharedState.with { |redis| redis.set(expected_key, result_hash.to_json) } + + expect(redis_interface.retrieve_check(merge_check: merge_check)).to eq result_hash + end + end +end diff --git a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb new file mode 100644 index 00000000000..d376dcb5b18 --- /dev/null +++ b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::MergeRequests::Mergeability::ResultsStore do + subject(:results_store) { described_class.new(merge_request: merge_request, interface: interface) } + + let(:merge_check) { double } + let(:interface) { double } + let(:merge_request) { double } + + describe '#read' do + it 'calls #retrieve on the interface' do + expect(interface).to receive(:retrieve_check).with(merge_check: merge_check) + + results_store.read(merge_check: merge_check) + end + end + + describe '#write' do + let(:result_hash) { double } + + it 'calls #save_check on the interface' do + expect(interface).to receive(:save_check).with(merge_check: merge_check, result_hash: result_hash) + + results_store.write(merge_check: merge_check, result_hash: result_hash) + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1eb54ee73f9..3711f304bd2 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3089,7 +3089,7 @@ RSpec.describe MergeRequest, factory_default: :keep do end end - describe '#mergeable_state?' do + shared_examples 'for mergeable_state' do subject { create(:merge_request) } it 'checks if merge request can be merged' do @@ -3130,33 +3130,61 @@ RSpec.describe MergeRequest, factory_default: :keep do end context 'when failed' do - context 'when #mergeable_ci_state? is false' do - before do - allow(subject).to receive(:mergeable_ci_state?) { false } - end + shared_examples 'failed skip_ci_check' do + context 'when #mergeable_ci_state? is false' do + before do + allow(subject).to receive(:mergeable_ci_state?) { false } + end - it 'returns false' do - expect(subject.mergeable_state?).to be_falsey + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + + it 'returns true when skipping ci check' do + expect(subject.mergeable_state?(skip_ci_check: true)).to be(true) + end end - it 'returns true when skipping ci check' do - expect(subject.mergeable_state?(skip_ci_check: true)).to be(true) + context 'when #mergeable_discussions_state? is false' do + before do + allow(subject).to receive(:mergeable_discussions_state?) { false } + end + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + + it 'returns true when skipping discussions check' do + expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true) + end end end - context 'when #mergeable_discussions_state? is false' do + context 'when improved_mergeability_checks is on' do + it_behaves_like 'failed skip_ci_check' + end + + context 'when improved_mergeability_checks is off' do before do - allow(subject).to receive(:mergeable_discussions_state?) { false } + stub_feature_flags(improved_mergeability_checks: false) end - it 'returns false' do - expect(subject.mergeable_state?).to be_falsey - end + it_behaves_like 'failed skip_ci_check' + end + end + end - it 'returns true when skipping discussions check' do - expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true) - end + describe '#mergeable_state?' do + context 'when merge state caching is on' do + it_behaves_like 'for mergeable_state' + end + + context 'when merge state caching is off' do + before do + stub_feature_flags(mergeability_caching: false) end + + it_behaves_like 'for mergeable_state' end end diff --git a/spec/services/merge_requests/mergeability/check_base_service_spec.rb b/spec/services/merge_requests/mergeability/check_base_service_spec.rb new file mode 100644 index 00000000000..f07522b43cb --- /dev/null +++ b/spec/services/merge_requests/mergeability/check_base_service_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::CheckBaseService do + subject(:check_base_service) { described_class.new(merge_request: merge_request, params: params) } + + let(:merge_request) { double } + let(:params) { double } + + describe '#merge_request' do + it 'returns the merge_request' do + expect(check_base_service.merge_request).to eq merge_request + end + end + + describe '#params' do + it 'returns the params' do + expect(check_base_service.params).to eq params + end + end + + describe '#skip?' do + it 'raises NotImplementedError' do + expect { check_base_service.skip? }.to raise_error(NotImplementedError) + end + end + + describe '#cacheable?' do + it 'raises NotImplementedError' do + expect { check_base_service.skip? }.to raise_error(NotImplementedError) + end + end + + describe '#cache_key?' do + it 'raises NotImplementedError' do + expect { check_base_service.skip? }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb new file mode 100644 index 00000000000..6fbbecd7c0e --- /dev/null +++ b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do + subject(:check_ci_status) { described_class.new(merge_request: merge_request, params: params) } + + let(:merge_request) { build(:merge_request) } + let(:params) { { skip_ci_check: skip_check } } + let(:skip_check) { false } + + describe '#execute' do + before do + expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable) + end + + context 'when the merge request is in a mergable state' do + let(:mergeable) { true } + + it 'returns a check result with status success' do + expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + end + end + + context 'when the merge request is not in a mergeable state' do + let(:mergeable) { false } + + it 'returns a check result with status failed' do + expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + end + end + end + + describe '#skip?' do + context 'when skip check is true' do + let(:skip_check) { true } + + it 'returns true' do + expect(check_ci_status.skip?).to eq true + end + end + + context 'when skip check is false' do + let(:skip_check) { false } + + it 'returns false' do + expect(check_ci_status.skip?).to eq false + end + end + end + + describe '#cacheable?' do + it 'returns false' do + expect(check_ci_status.cacheable?).to eq false + end + end +end diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb new file mode 100644 index 00000000000..170d99f4642 --- /dev/null +++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::RunChecksService do + subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) } + + let_it_be(:merge_request) { create(:merge_request) } + + describe '#CHECKS' do + it 'contains every subclass of the base checks service' do + expect(described_class::CHECKS).to contain_exactly(*MergeRequests::Mergeability::CheckBaseService.subclasses) + end + end + + describe '#execute' do + subject(:execute) { run_checks.execute } + + let(:params) { {} } + let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success } + + context 'when every check is skipped' do + before do + MergeRequests::Mergeability::CheckBaseService.subclasses.each do |subclass| + expect_next_instance_of(subclass) do |service| + expect(service).to receive(:skip?).and_return(true) + end + end + end + + it 'is still a success' do + expect(execute.all?(&:success?)).to eq(true) + end + end + + context 'when a check is skipped' do + it 'does not execute the check' do + expect_next_instance_of(MergeRequests::Mergeability::CheckCiStatusService) do |service| + expect(service).to receive(:skip?).and_return(true) + expect(service).not_to receive(:execute) + end + + expect(execute).to match_array([]) + end + end + + context 'when a check is not skipped' do + let(:cacheable) { true } + let(:merge_check) { instance_double(MergeRequests::Mergeability::CheckCiStatusService) } + + before do + expect(MergeRequests::Mergeability::CheckCiStatusService).to receive(:new).and_return(merge_check) + expect(merge_check).to receive(:skip?).and_return(false) + allow(merge_check).to receive(:cacheable?).and_return(cacheable) + allow(merge_check).to receive(:execute).and_return(success_result) + end + + context 'when the check is cacheable' do + context 'when the check is cached' do + it 'returns the cached result' do + expect_next_instance_of(Gitlab::MergeRequests::Mergeability::ResultsStore) do |service| + expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result) + end + + expect(execute).to match_array([success_result]) + end + end + + context 'when the check is not cached' do + it 'writes and returns the result' do + expect_next_instance_of(Gitlab::MergeRequests::Mergeability::ResultsStore) do |service| + expect(service).to receive(:read).with(merge_check: merge_check).and_return(nil) + expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true) + end + + expect(execute).to match_array([success_result]) + end + end + end + + context 'when check is not cacheable' do + let(:cacheable) { false } + + it 'does not call the results store' do + expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) + + expect(execute).to match_array([success_result]) + end + end + + context 'when mergeability_caching is turned off' do + before do + stub_feature_flags(mergeability_caching: false) + end + + it 'does not call the results store' do + expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) + + expect(execute).to match_array([success_result]) + end + end + end + end +end diff --git a/spec/support/database/cross-database-modification-allowlist.yml b/spec/support/database/cross-database-modification-allowlist.yml index 87126bdcdc8..819a77a697b 100644 --- a/spec/support/database/cross-database-modification-allowlist.yml +++ b/spec/support/database/cross-database-modification-allowlist.yml @@ -1338,3 +1338,4 @@ - "./spec/workers/repository_cleanup_worker_spec.rb" - "./spec/workers/stage_update_worker_spec.rb" - "./spec/workers/stuck_merge_jobs_worker_spec.rb" +- "./ee/spec/requests/api/graphql/project/pipelines/dast_profile_spec.rb" diff --git a/spec/support/database/cross-join-allowlist.yml b/spec/support/database/cross-join-allowlist.yml index de4d2f8156a..87a91c80671 100644 --- a/spec/support/database/cross-join-allowlist.yml +++ b/spec/support/database/cross-join-allowlist.yml @@ -7,7 +7,6 @@ - "./ee/spec/features/merge_trains/user_adds_to_merge_train_when_pipeline_succeeds_spec.rb" - "./ee/spec/features/projects/pipelines/pipeline_spec.rb" - "./ee/spec/finders/ee/namespaces/projects_finder_spec.rb" -- "./ee/spec/finders/security/findings_finder_spec.rb" - "./ee/spec/graphql/ee/resolvers/namespace_projects_resolver_spec.rb" - "./ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb" - "./ee/spec/lib/ee/gitlab/background_migration/migrate_security_scans_spec.rb" diff --git a/yarn.lock b/yarn.lock index d7d01640be6..6612d030c1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11006,10 +11006,10 @@ svg-tags@^1.0.0: resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= -swagger-ui-dist@^3.44.1: - version "3.44.1" - resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.44.1.tgz#757385a79698b8ef7045287be585671db4e4a252" - integrity sha512-N0u+aN55bp53RRwi/wFbEbkQxcHqZ445ShZR/Ct1Jg+XCMxYtocrsGavh7kdNKw5+6Rs4QDD6GzUMiT28Z1u3Q== +swagger-ui-dist@^3.52.3: + version "3.52.3" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.52.3.tgz#a09b5cdccac69e3f5f1cbd258654a110119a7f0e" + integrity sha512-7QSY4milmYx5O8dbzU5tTftiaoZt+4JGxahTTBiLAnbTvhTyzum9rsjDIJjC+xeT8Tt1KfB38UuQQjmrh2THDQ== symbol-observable@^1.0.2: version "1.2.0" |