Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-11 09:13:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-11 09:13:09 +0300
commitbe7d70b884e6fa66c52862f38bf0f39b0631868b (patch)
tree235616671718bf2f39855f663677b61a55a8d68c
parent848ba57883b4ea9164bcb56a16c0fcb2b55b56e6 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js7
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js14
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js27
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue45
-rw-r--r--app/assets/javascripts/repository/components/new_directory_modal.vue183
-rw-r--r--app/assets/javascripts/repository/constants.js3
-rw-r--r--app/assets/javascripts/repository/index.js1
-rw-r--r--app/assets/javascripts/repository/router.js21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue12
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/graphql/resolvers/project_pipelines_resolver.rb2
-rw-r--r--app/models/merge_request.rb12
-rw-r--r--app/services/merge_requests/mergeability/check_base_service.rb36
-rw-r--r--app/services/merge_requests/mergeability/check_ci_status_service.rb22
-rw-r--r--app/services/merge_requests/mergeability/run_checks_service.rb54
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/views/layouts/_startup_js.html.haml1
-rw-r--r--app/views/projects/_files.html.haml3
-rw-r--r--app/views/projects/blob/show.html.haml1
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml1
-rw-r--r--app/views/projects/tree/show.html.haml1
-rw-r--r--app/views/shared/_web_ide_path.html.haml4
-rw-r--r--config/feature_flags/development/improved_mergeability_checks.yml8
-rw-r--r--config/feature_flags/development/mergeability_caching.yml8
-rw-r--r--config/feature_flags/development/new_dir_modal.yml (renamed from config/feature_flags/development/vulnerability_flags.yml)10
-rw-r--r--db/post_migrate/20211006145004_finalize_indexes_for_ci_job_artifacts_expire_at_unlocked.rb16
-rw-r--r--db/schema_migrations/202110061450041
-rw-r--r--db/structure.sql2
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/update/index.md5
-rw-r--r--doc/user/application_security/sast/index.md9
-rw-r--r--lib/gitlab/email/handler/create_merge_request_handler.rb4
-rw-r--r--lib/gitlab/merge_requests/mergeability/check_result.rb48
-rw-r--r--lib/gitlab/merge_requests/mergeability/redis_interface.rb23
-rw-r--r--lib/gitlab/merge_requests/mergeability/results_store.rb25
-rw-r--r--lib/gitlab/quick_actions/merge_request_actions.rb6
-rw-r--r--lib/system_check/app/git_version_check.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--package.json2
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb2
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js35
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js38
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js203
-rw-r--r--spec/frontend/repository/router_spec.js28
-rw-r--r--spec/graphql/resolvers/project_pipelines_resolver_spec.rb20
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb140
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb29
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb29
-rw-r--r--spec/models/merge_request_spec.rb62
-rw-r--r--spec/services/merge_requests/mergeability/check_base_service_spec.rb40
-rw-r--r--spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb57
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb104
-rw-r--r--spec/support/database/cross-database-modification-allowlist.yml1
-rw-r--r--spec/support/database/cross-join-allowlist.yml1
-rw-r--r--yarn.lock8
60 files changed, 1368 insertions, 70 deletions
diff --git a/Gemfile b/Gemfile
index 8d20e98a5c8..1e6648df48b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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"