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:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue15
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js10
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue109
-rw-r--r--app/assets/javascripts/pipelines/components/unwrapping_utils.js17
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/utils.js44
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/graphql/mutations/boards/common_mutation_arguments.rb24
-rw-r--r--app/graphql/mutations/boards/create.rb26
-rw-r--r--app/graphql/mutations/boards/update.rb43
-rw-r--r--app/graphql/types/board_type.rb6
-rw-r--r--app/helpers/groups_helper.rb1
-rw-r--r--app/models/cycle_analytics/level_base.rb2
-rw-r--r--app/models/label_priority.rb5
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/resource_event.rb8
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/sentry_issue.rb5
-rw-r--r--app/models/suggestion.rb3
-rw-r--r--app/models/timelog.rb4
-rw-r--r--app/models/zoom_meeting.rb10
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--changelogs/unreleased/224698-fj-add-operations-access-level-to-api-project-settings.yml5
-rw-r--r--changelogs/unreleased/292466-add-hidebackloglist-and-hideclosedlist-to-createboard-mutation-inp.yml5
-rw-r--r--changelogs/unreleased/293024-fix-httparty-basic-auth.yml5
-rw-r--r--changelogs/unreleased/293866-ide_controller_spec.yml5
-rw-r--r--changelogs/unreleased/enable-new_project_level_vsa_backend-by-default.yml5
-rw-r--r--changelogs/unreleased/fix-feature-api-logging.yml5
-rw-r--r--changelogs/unreleased/mjang-ux-MR-approvals-text.yml5
-rw-r--r--changelogs/unreleased/pages-1-32.yml5
-rw-r--r--config/feature_flags/development/ci_config_visualization_tab.yml8
-rw-r--r--config/feature_flags/development/group_ci_cd_analytics_page.yml8
-rw-r--r--config/feature_flags/development/merge_request_reviewers.yml2
-rw-r--r--config/feature_flags/development/new_project_level_vsa_backend.yml2
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt1
-rw-r--r--doc/administration/audit_events.md12
-rw-r--r--doc/api/applications.md4
-rw-r--r--doc/api/broadcast_messages.md4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql54
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json130
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/projects.md3
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/ci/yaml/img/ci_config_visualization_hover_v13_5.pngbin30986 -> 0 bytes
-rw-r--r--doc/ci/yaml/img/ci_config_visualization_hover_v13_7.pngbin0 -> 19870 bytes
-rw-r--r--doc/ci/yaml/img/ci_config_visualization_v13_5.pngbin30319 -> 0 bytes
-rw-r--r--doc/ci/yaml/img/ci_config_visualization_v13_7.pngbin0 -> 18585 bytes
-rw-r--r--doc/ci/yaml/visualization.md16
-rw-r--r--doc/development/api_styleguide.md16
-rw-r--r--doc/development/cached_queries.md2
-rw-r--r--doc/development/deprecation_guidelines/index.md2
-rw-r--r--doc/development/diffs.md2
-rw-r--r--doc/development/emails.md2
-rw-r--r--doc/development/fe_guide/security.md2
-rw-r--r--doc/development/multi_version_compatibility.md2
-rw-r--r--doc/development/query_recorder.md2
-rw-r--r--doc/development/renaming_features.md6
-rw-r--r--doc/development/shared_files.md2
-rw-r--r--doc/raketasks/backup_restore.md6
-rw-r--r--doc/user/permissions.md6
-rw-r--r--doc/user/project/members/index.md2
-rw-r--r--doc/user/project/merge_requests/getting_started.md21
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md14
-rw-r--r--doc/user/project/requirements/index.md79
-rw-r--r--doc/user/project/settings/index.md3
-rw-r--r--lib/api/entities/project.rb1
-rw-r--r--lib/api/features.rb16
-rw-r--r--lib/api/helpers/projects_helpers.rb1
-rw-r--r--locale/gitlab.pot66
-rw-r--r--package.json2
-rw-r--r--rubocop/cop/rspec/httparty_basic_auth.rb49
-rw-r--r--spec/features/file_uploads/git_lfs_spec.rb2
-rw-r--r--spec/features/file_uploads/multipart_invalid_uploads_spec.rb2
-rw-r--r--spec/features/file_uploads/nuget_package_spec.rb2
-rw-r--r--spec/features/groups/navbar_spec.rb1
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js97
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js10
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js148
-rw-r--r--spec/frontend/pipelines/pipeline_graph/utils_spec.js79
-rw-r--r--spec/frontend/pipelines/unwrapping_utils_spec.js24
-rw-r--r--spec/helpers/projects_helper_spec.rb1
-rw-r--r--spec/models/label_priority_spec.rb6
-rw-r--r--spec/models/sentry_issue_spec.rb6
-rw-r--r--spec/models/suggestion_spec.rb6
-rw-r--r--spec/models/timelog_spec.rb8
-rw-r--r--spec/models/zoom_meeting_spec.rb14
-rw-r--r--spec/requests/api/features_spec.rb12
-rw-r--r--spec/requests/api/projects_spec.rb7
-rw-r--r--spec/requests/ide_controller_spec.rb (renamed from spec/controllers/ide_controller_spec.rb)2
-rw-r--r--spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb42
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb26
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb24
-rw-r--r--yarn.lock8
102 files changed, 982 insertions, 507 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 540f039a42f..3467de900c9 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-993f4ccd104882e0766bd1377f24642603bd558f
+bca59804605b4afd0bebacbaa0952c5b4ca16141
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 34aae156b19..359c41089a4 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.31.0
+1.32.0
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
new file mode 100644
index 00000000000..70bab8092c0
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -0,0 +1,2 @@
+export const CI_CONFIG_STATUS_VALID = 'VALID';
+export const CI_CONFIG_STATUS_INVALID = 'INVALID';
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 8a57c9b1970..96dc782964b 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -2,6 +2,7 @@
import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { mergeUrlParams, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import CommitForm from './components/commit/commit_form.vue';
@@ -31,6 +32,7 @@ export default {
PipelineGraph,
TextEditor,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
projectPath: {
type: String,
@@ -115,6 +117,9 @@ export default {
isBlobContentLoading() {
return this.$apollo.queries.content.loading;
},
+ isVisualizationTabLoading() {
+ return this.$apollo.queries.ciConfigData.loading;
+ },
isVisualizeTabActive() {
return this.currentTabIndex === 1;
},
@@ -266,8 +271,14 @@ export default {
<text-editor v-model="contentModel" @editor-ready="editorIsReady = true" />
</gl-tab>
- <gl-tab :title="$options.i18n.tabGraph" :lazy="!isVisualizeTabActive">
- <pipeline-graph :pipeline-data="ciConfigData" />
+ <gl-tab
+ v-if="glFeatures.ciConfigVisualizationTab"
+ :title="$options.i18n.tabGraph"
+ :lazy="!isVisualizeTabActive"
+ data-testid="visualization-tab"
+ >
+ <gl-loading-icon v-if="isVisualizationTabLoading" size="lg" class="gl-m-3" />
+ <pipeline-graph v-else :pipeline-data="ciConfigData" />
</gl-tab>
</gl-tabs>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js b/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
index 45940d4a39c..35230e1511b 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js
@@ -1,5 +1,5 @@
import * as d3 from 'd3';
-import { createUniqueJobId } from '../../utils';
+import { createUniqueLinkId } from '../../utils';
/**
* This function expects its first argument data structure
* to be the same shaped as the one generated by `parseData`,
@@ -12,13 +12,13 @@ import { createUniqueJobId } from '../../utils';
* @returns {Array} Links that contain all the information about them
*/
-export const generateLinksData = ({ links }, jobs, containerID) => {
+export const generateLinksData = ({ links }, containerID) => {
const containerEl = document.getElementById(containerID);
return links.map(link => {
const path = d3.path();
- const sourceId = jobs[link.source].id;
- const targetId = jobs[link.target].id;
+ const sourceId = link.source;
+ const targetId = link.target;
const sourceNodeEl = document.getElementById(sourceId);
const targetNodeEl = document.getElementById(targetId);
@@ -89,7 +89,7 @@ export const generateLinksData = ({ links }, jobs, containerID) => {
...link,
source: sourceId,
target: targetId,
- ref: createUniqueJobId(sourceId, targetId),
+ ref: createUniqueLinkId(sourceId, targetId),
path: path.toString(),
};
});
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
index a0c35f54c0e..51a95612d3f 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
@@ -10,10 +10,6 @@ export default {
type: String,
required: true,
},
- jobId: {
- type: String,
- required: true,
- },
isHighlighted: {
type: Boolean,
required: false,
@@ -45,7 +41,7 @@ export default {
},
methods: {
onMouseEnter() {
- this.$emit('on-mouse-enter', this.jobId);
+ this.$emit('on-mouse-enter', this.jobName);
},
onMouseLeave() {
this.$emit('on-mouse-leave');
@@ -56,7 +52,7 @@ export default {
<template>
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
- :id="jobId"
+ :id="jobName"
class="gl-w-15 gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
:class="jobPillClasses"
@mouseover="onMouseEnter"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
index 11ad2f2a3b6..73e5f2542fb 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -6,8 +6,10 @@ import JobPill from './job_pill.vue';
import StagePill from './stage_pill.vue';
import { generateLinksData } from './drawing_utils';
import { parseData } from '../parsing_utils';
-import { DRAW_FAILURE, DEFAULT } from '../../constants';
-import { generateJobNeedsDict } from '../../utils';
+import { unwrapArrayOfJobs } from '../unwrapping_utils';
+import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants';
+import { createJobsHash, generateJobNeedsDict } from '../../utils';
+import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
export default {
components: {
@@ -22,6 +24,12 @@ export default {
[DRAW_FAILURE]: __('Could not draw the lines for job relationships'),
[DEFAULT]: __('An unknown error occurred.'),
},
+ warningTexts: {
+ [EMPTY_PIPELINE_DATA]: __(
+ 'The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax.',
+ ),
+ [INVALID_CI_CONFIG]: __('Your CI configuration file is invalid.'),
+ },
props: {
pipelineData: {
required: true,
@@ -40,18 +48,51 @@ export default {
},
computed: {
isPipelineDataEmpty() {
- return isEmpty(this.pipelineData);
+ return !this.isInvalidCiConfig && isEmpty(this.pipelineData?.stages);
+ },
+ isInvalidCiConfig() {
+ return this.pipelineData?.status === CI_CONFIG_STATUS_INVALID;
+ },
+ showAlert() {
+ return this.hasError || this.hasWarning;
},
hasError() {
return this.failureType;
},
+ hasWarning() {
+ return this.warning;
+ },
hasHighlightedJob() {
return Boolean(this.highlightedJob);
},
+ alert() {
+ if (this.hasError) {
+ return this.failure;
+ }
+
+ return this.warning;
+ },
failure() {
const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT];
- return { text, variant: 'danger' };
+ return { text, variant: 'danger', dismissible: true };
+ },
+ warning() {
+ if (this.isPipelineDataEmpty) {
+ return {
+ text: this.$options.warningTexts[EMPTY_PIPELINE_DATA],
+ variant: 'tip',
+ dismissible: false,
+ };
+ } else if (this.isInvalidCiConfig) {
+ return {
+ text: this.$options.warningTexts[INVALID_CI_CONFIG],
+ variant: 'danger',
+ dismissible: false,
+ };
+ }
+
+ return null;
},
viewBox() {
return [0, 0, this.width, this.height];
@@ -80,19 +121,21 @@ export default {
},
},
mounted() {
- if (!this.isPipelineDataEmpty) {
- this.getGraphDimensions();
- this.drawJobLinks();
+ if (!this.isPipelineDataEmpty && !this.isInvalidCiConfig) {
+ // This guarantee that all sub-elements are rendered
+ // https://v3.vuejs.org/api/options-lifecycle-hooks.html#mounted
+ this.$nextTick(() => {
+ this.getGraphDimensions();
+ this.prepareLinkData();
+ });
}
},
methods: {
- drawJobLinks() {
- const { stages, jobs } = this.pipelineData;
- const unwrappedGroups = this.unwrapPipelineData(stages);
-
+ prepareLinkData() {
try {
- const parsedData = parseData(unwrappedGroups);
- this.links = generateLinksData(parsedData, jobs, this.$options.CONTAINER_ID);
+ const arrayOfJobs = unwrapArrayOfJobs(this.pipelineData);
+ const parsedData = parseData(arrayOfJobs);
+ this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID);
} catch {
this.reportFailure(DRAW_FAILURE);
}
@@ -119,7 +162,8 @@ export default {
// The first time we hover, we create the object where
// we store all the data to properly highlight the needs.
if (!this.needsObject) {
- this.needsObject = generateJobNeedsDict(this.pipelineData) ?? {};
+ const jobs = createJobsHash(this.pipelineData);
+ this.needsObject = generateJobNeedsDict(jobs) ?? {};
}
this.highlightedJob = uniqueJobId;
@@ -127,18 +171,9 @@ export default {
removeHighlightNeeds() {
this.highlightedJob = null;
},
- unwrapPipelineData(stages) {
- return stages
- .map(({ name, groups }) => {
- return groups.map(group => {
- return { category: name, ...group };
- });
- })
- .flat(2);
- },
getGraphDimensions() {
- this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}px`;
- this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}px`;
+ this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}`;
+ this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}`;
},
reportFailure(errorType) {
this.failureType = errorType;
@@ -163,21 +198,20 @@ export default {
</script>
<template>
<div>
- <gl-alert v-if="hasError" :variant="failure.variant" @dismiss="resetFailure">
- {{ failure.text }}
- </gl-alert>
- <gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false">
- {{
- __(
- 'The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax.',
- )
- }}
+ <gl-alert
+ v-if="showAlert"
+ :variant="alert.variant"
+ :dismissible="alert.dismissible"
+ @dismiss="alert.dismissible ? resetFailure : null"
+ >
+ {{ alert.text }}
</gl-alert>
<div
- v-else
+ v-if="!hasWarning"
:id="$options.CONTAINER_ID"
:ref="$options.CONTAINER_REF"
class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7"
+ data-testid="graph-container"
>
<svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
<template>
@@ -210,10 +244,9 @@ export default {
<job-pill
v-for="group in stage.groups"
:key="group.name"
- :job-id="group.id"
:job-name="group.name"
- :is-highlighted="hasHighlightedJob && isJobHighlighted(group.id)"
- :is-faded-out="hasHighlightedJob && !isJobHighlighted(group.id)"
+ :is-highlighted="hasHighlightedJob && isJobHighlighted(group.name)"
+ :is-faded-out="hasHighlightedJob && !isJobHighlighted(group.name)"
@on-mouse-enter="highlightNeeds"
@on-mouse-leave="removeHighlightNeeds"
/>
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
index 99934cd5014..aa33f622ce6 100644
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
@@ -1,3 +1,20 @@
+/**
+ * This function takes the stages and add the stage name
+ * at the group level as `category` to have an easier
+ * implementation while constructions nodes with D3
+ * @param {Array} stages
+ * @returns {Array} - Array of stages with stage name at the group level as `category`
+ */
+export const unwrapArrayOfJobs = (stages = []) => {
+ return stages
+ .map(({ name, groups }) => {
+ return groups.map(group => {
+ return { category: name, ...group };
+ });
+ })
+ .flat(2);
+};
+
const unwrapGroups = stages => {
return stages.map(stage => {
const {
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 607e7a66f44..757d285ef19 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -28,6 +28,8 @@ export const RAW_TEXT_WARNING = s__(
export const DEFAULT = 'default';
export const DELETE_FAILURE = 'delete_pipeline_failure';
export const DRAW_FAILURE = 'draw_failure';
+export const EMPTY_PIPELINE_DATA = 'empty_data';
+export const INVALID_CI_CONFIG = 'invalid_ci_config';
export const LOAD_FAILURE = 'load_failure';
export const PARSE_FAILURE = 'parse_failure';
export const POST_FAILURE = 'post_failure';
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 46e54bfb4ff..28d6c0edb0f 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -5,9 +5,42 @@ export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
-export const createUniqueJobId = (stageName, jobName) => `${stageName}-${jobName}`;
+export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;
-export const generateJobNeedsDict = ({ jobs }) => {
+/**
+ * This function takes the stages array and transform it
+ * into a hash where each key is a job name and the job data
+ * is associated to that key.
+ * @param {Array} stages
+ * @returns {Object} - Hash of jobs
+ */
+export const createJobsHash = (stages = []) => {
+ const jobsHash = {};
+
+ stages.forEach(stage => {
+ if (stage.groups.length > 0) {
+ stage.groups.forEach(group => {
+ group.jobs.forEach(job => {
+ jobsHash[job.name] = job;
+ });
+ });
+ }
+ });
+
+ return jobsHash;
+};
+
+/**
+ * This function takes the jobs hash generated by
+ * `createJobsHash` function and returns an easier
+ * structure to work with for needs relationship
+ * where the key is the job name and the value is an
+ * array of all the needs this job has recursively
+ * (includes the needs of the needs)
+ * @param {Object} jobs
+ * @returns {Object} - Hash of jobs and array of needs
+ */
+export const generateJobNeedsDict = (jobs = {}) => {
const arrOfJobNames = Object.keys(jobs);
return arrOfJobNames.reduce((acc, value) => {
@@ -18,13 +51,12 @@ export const generateJobNeedsDict = ({ jobs }) => {
return jobs[jobName].needs
.map(job => {
- const { id } = jobs[job];
// If we already have the needs of a job in the accumulator,
// then we use the memoized data instead of the recursive call
// to save some performance.
- const newNeeds = acc[id] ?? recursiveNeeds(job);
+ const newNeeds = acc[job] ?? recursiveNeeds(job);
- return [id, ...newNeeds];
+ return [job, ...newNeeds];
})
.flat(Infinity);
};
@@ -34,6 +66,6 @@ export const generateJobNeedsDict = ({ jobs }) => {
// duplicates from the array.
const uniqueValues = Array.from(new Set(recursiveNeeds(value)));
- return { ...acc, [jobs[value].id]: uniqueValues };
+ return { ...acc, [value]: uniqueValues };
}, {});
};
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index c2428270fa6..cc391868df0 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -2,6 +2,9 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
+ before_action do
+ push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: false)
+ end
feature_category :pipeline_authoring
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 453928e251f..2aa7e9038e9 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -12,7 +12,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
before_action :build_merge_request, except: [:create]
before_action do
- push_frontend_feature_flag(:merge_request_reviewers, @project)
+ push_frontend_feature_flag(:merge_request_reviewers, @project, default_enabled: true)
push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ee25a2006ad..b48aa02a81b 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -51,7 +51,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
- push_frontend_feature_flag(:merge_request_reviewers, @project)
+ push_frontend_feature_flag(:merge_request_reviewers, @project, default_enabled: true)
push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
end
diff --git a/app/graphql/mutations/boards/common_mutation_arguments.rb b/app/graphql/mutations/boards/common_mutation_arguments.rb
new file mode 100644
index 00000000000..c4f8d299318
--- /dev/null
+++ b/app/graphql/mutations/boards/common_mutation_arguments.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module CommonMutationArguments
+ extend ActiveSupport::Concern
+
+ included do
+ argument :name,
+ GraphQL::STRING_TYPE,
+ required: false,
+ description: 'The board name.'
+ argument :hide_backlog_list,
+ GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: copy_field_description(Types::BoardType, :hide_backlog_list)
+ argument :hide_closed_list,
+ GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: copy_field_description(Types::BoardType, :hide_closed_list)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/boards/create.rb b/app/graphql/mutations/boards/create.rb
index ebbd19930ec..92bce557446 100644
--- a/app/graphql/mutations/boards/create.rb
+++ b/app/graphql/mutations/boards/create.rb
@@ -7,36 +7,18 @@ module Mutations
graphql_name 'CreateBoard'
+ include Mutations::Boards::CommonMutationArguments
+
field :board,
Types::BoardType,
null: true,
description: 'The board after mutation.'
- argument :name,
- GraphQL::STRING_TYPE,
- required: false,
- description: 'The board name.'
- argument :assignee_id,
- GraphQL::STRING_TYPE,
- required: false,
- description: 'The ID of the user to be assigned to the board.'
- argument :milestone_id,
- Types::GlobalIDType[Milestone],
- required: false,
- description: 'The ID of the milestone to be assigned to the board.'
- argument :weight,
- GraphQL::BOOLEAN_TYPE,
- required: false,
- description: 'The weight of the board.'
- argument :label_ids,
- [Types::GlobalIDType[Label]],
- required: false,
- description: 'The IDs of labels to be added to the board.'
-
authorize :admin_board
def resolve(args)
board_parent = authorized_resource_parent_find!(args)
+
response = ::Boards::CreateService.new(board_parent, current_user, args).execute
{
@@ -47,3 +29,5 @@ module Mutations
end
end
end
+
+Mutations::Boards::Create.prepend_if_ee('::EE::Mutations::Boards::Create')
diff --git a/app/graphql/mutations/boards/update.rb b/app/graphql/mutations/boards/update.rb
new file mode 100644
index 00000000000..5cb434e41fd
--- /dev/null
+++ b/app/graphql/mutations/boards/update.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ class Update < ::Mutations::BaseMutation
+ graphql_name 'UpdateBoard'
+
+ include Mutations::Boards::CommonMutationArguments
+
+ argument :id,
+ ::Types::GlobalIDType[::Board],
+ required: true,
+ description: 'The board global ID.'
+
+ field :board,
+ Types::BoardType,
+ null: true,
+ description: 'The board after mutation.'
+
+ authorize :admin_board
+
+ def resolve(id:, **args)
+ board = authorized_find!(id: id)
+
+ ::Boards::UpdateService.new(board.resource_parent, current_user, args).execute(board)
+
+ {
+ board: board,
+ errors: errors_on_object(board)
+ }
+ end
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ::Types::GlobalIDType[::Board].coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+end
+
+Mutations::Boards::Update.prepend_if_ee('::EE::Mutations::Boards::Update')
diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb
index 2a7b318e283..f47c744d1bb 100644
--- a/app/graphql/types/board_type.rb
+++ b/app/graphql/types/board_type.rb
@@ -12,6 +12,12 @@ module Types
field :name, type: GraphQL::STRING_TYPE, null: true,
description: 'Name of the board'
+ field :hide_backlog_list, type: GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Whether or not backlog list is hidden'
+
+ field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Whether or not closed list is hidden'
+
field :lists,
Types::BoardListType.connection_type,
null: true,
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index a0a840add94..e8eb6a5d417 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -21,7 +21,6 @@ module GroupsHelper
integrations#edit
ldap_group_links#index
hooks#index
- audit_events#index
pipeline_quota#index
]
end
diff --git a/app/models/cycle_analytics/level_base.rb b/app/models/cycle_analytics/level_base.rb
index 63c55e2ca64..901636a7263 100644
--- a/app/models/cycle_analytics/level_base.rb
+++ b/app/models/cycle_analytics/level_base.rb
@@ -60,7 +60,7 @@ module CycleAnalytics
end
def [](stage_name)
- if Feature.enabled?(:new_project_level_vsa_backend, resource_parent)
+ if Feature.enabled?(:new_project_level_vsa_backend, resource_parent, default_enabled: true)
StageAdapter.new(build_stage(stage_name), options)
else
Gitlab::CycleAnalytics::Stage[stage_name].new(options: options)
diff --git a/app/models/label_priority.rb b/app/models/label_priority.rb
index 8f8f36efbfe..11854404a71 100644
--- a/app/models/label_priority.rb
+++ b/app/models/label_priority.rb
@@ -1,10 +1,13 @@
# frozen_string_literal: true
class LabelPriority < ApplicationRecord
+ include Importable
+
belongs_to :project
belongs_to :label
- validates :project, :label, :priority, presence: true
+ validates :label, presence: true, unless: :importing?
+ validates :project, :priority, presence: true
validates :label_id, uniqueness: { scope: :project_id }
validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 6231d8c9421..043f07cf9f3 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1755,7 +1755,7 @@ class MergeRequest < ApplicationRecord
end
def allows_reviewers?
- Feature.enabled?(:merge_request_reviewers, project)
+ Feature.enabled?(:merge_request_reviewers, project, default_enabled: true)
end
def allows_multiple_reviewers?
diff --git a/app/models/resource_event.rb b/app/models/resource_event.rb
index 26dcda2630a..54fa4137f73 100644
--- a/app/models/resource_event.rb
+++ b/app/models/resource_event.rb
@@ -30,14 +30,6 @@ class ResourceEvent < ApplicationRecord
return true if issuable_count == 1
- # if none of issuable IDs is set, check explicitly if nested issuable
- # object is set, this is used during project import
- if issuable_count == 0 && importing?
- issuable_count = self.class.issuable_attrs.count { |attr| self.public_send(attr) } # rubocop:disable GitlabSecurity/PublicSend
-
- return true if issuable_count == 1
- end
-
errors.add(
:base, _("Exactly one of %{attributes} is required") %
{ attributes: self.class.issuable_attrs.join(', ') }
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index cc96698be09..57a3b568c53 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -12,7 +12,7 @@ class ResourceLabelEvent < ResourceEvent
scope :inc_relations, -> { includes(:label, :user) }
validates :label, presence: { unless: :importing? }, on: :create
- validate :exactly_one_issuable
+ validate :exactly_one_issuable, unless: :importing?
after_save :expire_etag_cache
after_destroy :expire_etag_cache
diff --git a/app/models/sentry_issue.rb b/app/models/sentry_issue.rb
index 30f4026e633..fec1a55f17d 100644
--- a/app/models/sentry_issue.rb
+++ b/app/models/sentry_issue.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
class SentryIssue < ApplicationRecord
+ include Importable
+
belongs_to :issue
- validates :issue, uniqueness: true, presence: true
+ validates :issue, uniqueness: true
+ validates :issue, presence: true, unless: :importing?
validates :sentry_issue_identifier, presence: true
validate :ensure_sentry_issue_identifier_is_unique_per_project
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
index 8c72bd5ae7e..ff564d87449 100644
--- a/app/models/suggestion.rb
+++ b/app/models/suggestion.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
class Suggestion < ApplicationRecord
+ include Importable
include Suggestible
belongs_to :note, inverse_of: :suggestions
- validates :note, presence: true
+ validates :note, presence: true, unless: :importing?
validates :commit_id, presence: true, if: :applied?
delegate :position, :noteable, to: :note
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index 60aaaaef831..f4debedb656 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
class Timelog < ApplicationRecord
+ include Importable
+
validates :time_spent, :user, presence: true
- validate :issuable_id_is_present
+ validate :issuable_id_is_present, unless: :importing?
belongs_to :issue, touch: true
belongs_to :merge_request, touch: true
diff --git a/app/models/zoom_meeting.rb b/app/models/zoom_meeting.rb
index f83aa93b69a..c8b510c4779 100644
--- a/app/models/zoom_meeting.rb
+++ b/app/models/zoom_meeting.rb
@@ -1,13 +1,17 @@
# frozen_string_literal: true
class ZoomMeeting < ApplicationRecord
+ include Importable
include UsageStatistics
- belongs_to :project, optional: false
- belongs_to :issue, optional: false
+ belongs_to :project
+ belongs_to :issue
+
+ validates :project, presence: true, unless: :importing?
+ validates :issue, presence: true, unless: :importing?
validates :url, presence: true, length: { maximum: 255 }, zoom_url: true
- validates :issue, same_project_association: true
+ validates :issue, same_project_association: true, unless: :importing?
enum issue_status: {
added: 1,
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 3bc485cbdfc..4e39b5a05c0 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -437,8 +437,6 @@
%span
= _('Pages')
- = render_if_exists 'projects/sidebar/settings_audit_events'
-
= render 'shared/sidebar_toggle_button'
-# Shortcut to Project > Activity
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 2f7b5b93de7..4711143c900 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -55,7 +55,7 @@
- if merge_request.assignees.any?
%li.gl-display-flex.gl-align-items-center
= render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
- - if Feature.enabled?(:merge_request_reviewers, @project) && merge_request.reviewers.any?
+ - if Feature.enabled?(:merge_request_reviewers, @project, default_enabled: true) && merge_request.reviewers.any?
%li.gl-display-flex.issuable-reviewers
= render 'shared/issuable/reviewers', project: merge_request.project, issuable: merge_request
= render 'projects/merge_requests/approvals_count', merge_request: merge_request
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index fe507697321..cd265c10451 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -25,7 +25,7 @@
.block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- - if Feature.enabled?(:merge_request_reviewers, @project) && reviewers
+ - if Feature.enabled?(:merge_request_reviewers, @project, default_enabled: true) && reviewers
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
diff --git a/changelogs/unreleased/224698-fj-add-operations-access-level-to-api-project-settings.yml b/changelogs/unreleased/224698-fj-add-operations-access-level-to-api-project-settings.yml
new file mode 100644
index 00000000000..a80efab8c85
--- /dev/null
+++ b/changelogs/unreleased/224698-fj-add-operations-access-level-to-api-project-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add operations_access_level to project settings API
+merge_request: 50023
+author:
+type: added
diff --git a/changelogs/unreleased/292466-add-hidebackloglist-and-hideclosedlist-to-createboard-mutation-inp.yml b/changelogs/unreleased/292466-add-hidebackloglist-and-hideclosedlist-to-createboard-mutation-inp.yml
new file mode 100644
index 00000000000..bec6c7eeb3c
--- /dev/null
+++ b/changelogs/unreleased/292466-add-hidebackloglist-and-hideclosedlist-to-createboard-mutation-inp.yml
@@ -0,0 +1,5 @@
+---
+title: Allow updating `hideBacklogList` and `hideClosedList` board attributes
+merge_request: 49947
+author:
+type: changed
diff --git a/changelogs/unreleased/293024-fix-httparty-basic-auth.yml b/changelogs/unreleased/293024-fix-httparty-basic-auth.yml
new file mode 100644
index 00000000000..2094f5d73a5
--- /dev/null
+++ b/changelogs/unreleased/293024-fix-httparty-basic-auth.yml
@@ -0,0 +1,5 @@
+---
+title: Add custom cop to prevent invalid HTTParty usage
+merge_request: 49878
+author: Ethan Reesor (@firelizzard)
+type: fixed
diff --git a/changelogs/unreleased/293866-ide_controller_spec.yml b/changelogs/unreleased/293866-ide_controller_spec.yml
new file mode 100644
index 00000000000..c29dd517e6f
--- /dev/null
+++ b/changelogs/unreleased/293866-ide_controller_spec.yml
@@ -0,0 +1,5 @@
+---
+title: Replace spec/controllers/ide_controller_spec.rb with request spec
+merge_request: 50075
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/enable-new_project_level_vsa_backend-by-default.yml b/changelogs/unreleased/enable-new_project_level_vsa_backend-by-default.yml
new file mode 100644
index 00000000000..4c527992d38
--- /dev/null
+++ b/changelogs/unreleased/enable-new_project_level_vsa_backend-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Use the improved version of Value Stream Analytics backend on the project level
+merge_request: 50141
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-feature-api-logging.yml b/changelogs/unreleased/fix-feature-api-logging.yml
new file mode 100644
index 00000000000..2c8193583b0
--- /dev/null
+++ b/changelogs/unreleased/fix-feature-api-logging.yml
@@ -0,0 +1,5 @@
+---
+title: Fix feature flag logging is not working on API
+merge_request: 50025
+author:
+type: fixed
diff --git a/changelogs/unreleased/mjang-ux-MR-approvals-text.yml b/changelogs/unreleased/mjang-ux-MR-approvals-text.yml
new file mode 100644
index 00000000000..fc72705edd6
--- /dev/null
+++ b/changelogs/unreleased/mjang-ux-MR-approvals-text.yml
@@ -0,0 +1,5 @@
+---
+title: Updated UI text to match style guidelines
+merge_request: 49632
+author:
+type: other
diff --git a/changelogs/unreleased/pages-1-32.yml b/changelogs/unreleased/pages-1-32.yml
new file mode 100644
index 00000000000..fd37eccf3c9
--- /dev/null
+++ b/changelogs/unreleased/pages-1-32.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade GitLab Pages to 1.32.0
+merge_request: 50062
+author:
+type: added
diff --git a/config/feature_flags/development/ci_config_visualization_tab.yml b/config/feature_flags/development/ci_config_visualization_tab.yml
new file mode 100644
index 00000000000..70e395d83e9
--- /dev/null
+++ b/config/feature_flags/development/ci_config_visualization_tab.yml
@@ -0,0 +1,8 @@
+---
+name: ci_config_visualization_tab
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48793
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290117
+milestone: '13.7'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/group_ci_cd_analytics_page.yml b/config/feature_flags/development/group_ci_cd_analytics_page.yml
new file mode 100644
index 00000000000..9f25ca7e6ad
--- /dev/null
+++ b/config/feature_flags/development/group_ci_cd_analytics_page.yml
@@ -0,0 +1,8 @@
+---
+name: group_ci_cd_analytics_page
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49608
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292601
+milestone: '13.8'
+type: development
+group: group::release
+default_enabled: false
diff --git a/config/feature_flags/development/merge_request_reviewers.yml b/config/feature_flags/development/merge_request_reviewers.yml
index f2f7397c88d..85582ee25f9 100644
--- a/config/feature_flags/development/merge_request_reviewers.yml
+++ b/config/feature_flags/development/merge_request_reviewers.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/245190
milestone: '13.4'
type: development
group: group::code review
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/new_project_level_vsa_backend.yml b/config/feature_flags/development/new_project_level_vsa_backend.yml
index e631d9df518..988e5e39a03 100644
--- a/config/feature_flags/development/new_project_level_vsa_backend.yml
+++ b/config/feature_flags/development/new_project_level_vsa_backend.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282435
milestone: '13.7'
type: development
group: group::optimize
-default_enabled: false
+default_enabled: true
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index b8640581175..fd3aff63bce 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -347,6 +347,7 @@ prepending
prepends
Prettifier
Pritaly
+Priyanka
profiler
Prometheus
protobuf
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 16ad45dcc2e..7436661920a 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -44,10 +44,10 @@ Impersonation is where an administrator uses credentials to perform an action as
### Group events **(STARTER)**
-NOTE:
-You need Owner [permissions](../user/permissions.md) to view the group Audit Events page.
+A user with a Owner role (or above) can retrieve group audit events of all users.
+A user with a Developer or Maintainer role is limited to group audit events based on their individual actions.
-To view a group's audit events, navigate to **Group > Settings > Audit Events**.
+To view a group's audit events, navigate to **Group > Security & Compliance > Audit Events**.
From there, you can see the following actions:
- Group name or path changed.
@@ -74,10 +74,10 @@ Group events can also be accessed via the [Group Audit Events API](../api/audit_
### Project events **(STARTER)**
-NOTE:
-You need Maintainer [permissions](../user/permissions.md) or higher to view the project Audit Events page.
+A user with a Maintainer role (or above) can retrieve project audit events of all users.
+A user with a Developer role is limited to project audit events based on their individual actions.
-To view a project's audit events, navigate to **Project > Settings > Audit Events**.
+To view a project's audit events, navigate to **Project > Security & Compliance > Audit Events**.
From there, you can see the following actions:
- Added or removed deploy keys
diff --git a/doc/api/applications.md b/doc/api/applications.md
index c57ddb8471b..eb4930c8721 100644
--- a/doc/api/applications.md
+++ b/doc/api/applications.md
@@ -33,7 +33,7 @@ Parameters:
| `name` | string | yes | Name of the application. |
| `redirect_uri` | string | yes | Redirect URI of the application. |
| `scopes` | string | yes | Scopes of the application. |
-| `confidential` | boolean | no | The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential. Defaults to `true` if not supplied |
+| `confidential` | boolean | no | The application is used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential. Defaults to `true` if not supplied |
Example request:
@@ -83,7 +83,7 @@ Example response:
```
NOTE:
-The `secret` value will not be exposed by this API.
+The `secret` value is not exposed by this API.
## Delete an application
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
index 586961c8a01..04b2727f575 100644
--- a/doc/api/broadcast_messages.md
+++ b/doc/api/broadcast_messages.md
@@ -12,8 +12,8 @@ Broadcast messages API operates on [broadcast messages](../user/admin_area/broad
As of GitLab 12.8, GET requests do not require authentication. All other broadcast message API endpoints are accessible only to administrators. Non-GET requests by:
-- Guests will result in `401 Unauthorized`.
-- Regular users will result in `403 Forbidden`.
+- Guests result in `401 Unauthorized`.
+- Regular users result in `403 Forbidden`.
## Get all broadcast messages
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 858ec8f0bf5..b7e76987d49 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -3976,11 +3976,6 @@ Autogenerated input type of CreateBoard
"""
input CreateBoardInput {
"""
- The ID of the user to be assigned to the board.
- """
- assigneeId: String
-
- """
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
@@ -3991,14 +3986,14 @@ input CreateBoardInput {
groupPath: ID
"""
- The IDs of labels to be added to the board.
+ Whether or not backlog list is hidden
"""
- labelIds: [LabelID!]
+ hideBacklogList: Boolean
"""
- The ID of the milestone to be assigned to the board.
+ Whether or not closed list is hidden
"""
- milestoneId: MilestoneID
+ hideClosedList: Boolean
"""
The board name.
@@ -4009,11 +4004,6 @@ input CreateBoardInput {
The project full path the resource is associated with
"""
projectPath: ID
-
- """
- The weight of the board.
- """
- weight: Boolean
}
"""
@@ -23400,11 +23390,6 @@ Autogenerated input type of UpdateBoard
"""
input UpdateBoardInput {
"""
- The ID of user to be assigned to the board
- """
- assigneeId: UserID
-
- """
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
@@ -23420,39 +23405,14 @@ input UpdateBoardInput {
hideClosedList: Boolean
"""
- The board global ID
+ The board global ID.
"""
id: BoardID!
"""
- The ID of iteration to be assigned to the board.
- """
- iterationId: IterationID
-
- """
- The IDs of labels to be added to the board
- """
- labelIds: [LabelID!]
-
- """
- Labels of the issue
- """
- labels: [String!]
-
- """
- The ID of milestone to be assigned to the board
- """
- milestoneId: MilestoneID
-
- """
- Name of the board
+ The board name.
"""
name: String
-
- """
- The weight value to be assigned to the board
- """
- weight: Int
}
"""
@@ -23505,7 +23465,7 @@ Autogenerated return type of UpdateBoard
"""
type UpdateBoardPayload {
"""
- The board after mutation
+ The board after mutation.
"""
board: Board
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 4b4ef9c63d5..a8c20a1ee57 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -10945,28 +10945,18 @@
"defaultValue": null
},
{
- "name": "assigneeId",
- "description": "The ID of the user to be assigned to the board.",
- "type": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- },
- "defaultValue": null
- },
- {
- "name": "milestoneId",
- "description": "The ID of the milestone to be assigned to the board.",
+ "name": "hideBacklogList",
+ "description": "Whether or not backlog list is hidden",
"type": {
"kind": "SCALAR",
- "name": "MilestoneID",
+ "name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
- "name": "weight",
- "description": "The weight of the board.",
+ "name": "hideClosedList",
+ "description": "Whether or not closed list is hidden",
"type": {
"kind": "SCALAR",
"name": "Boolean",
@@ -10975,24 +10965,6 @@
"defaultValue": null
},
{
- "name": "labelIds",
- "description": "The IDs of labels to be added to the board.",
- "type": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "LabelID",
- "ofType": null
- }
- }
- },
- "defaultValue": null
- },
- {
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
@@ -68542,22 +68514,8 @@
"fields": null,
"inputFields": [
{
- "name": "id",
- "description": "The board global ID",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "BoardID",
- "ofType": null
- }
- },
- "defaultValue": null
- },
- {
"name": "name",
- "description": "Name of the board",
+ "description": "The board name.",
"type": {
"kind": "SCALAR",
"name": "String",
@@ -68586,77 +68544,15 @@
"defaultValue": null
},
{
- "name": "assigneeId",
- "description": "The ID of user to be assigned to the board",
- "type": {
- "kind": "SCALAR",
- "name": "UserID",
- "ofType": null
- },
- "defaultValue": null
- },
- {
- "name": "milestoneId",
- "description": "The ID of milestone to be assigned to the board",
- "type": {
- "kind": "SCALAR",
- "name": "MilestoneID",
- "ofType": null
- },
- "defaultValue": null
- },
- {
- "name": "iterationId",
- "description": "The ID of iteration to be assigned to the board.",
- "type": {
- "kind": "SCALAR",
- "name": "IterationID",
- "ofType": null
- },
- "defaultValue": null
- },
- {
- "name": "weight",
- "description": "The weight value to be assigned to the board",
- "type": {
- "kind": "SCALAR",
- "name": "Int",
- "ofType": null
- },
- "defaultValue": null
- },
- {
- "name": "labels",
- "description": "Labels of the issue",
- "type": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "String",
- "ofType": null
- }
- }
- },
- "defaultValue": null
- },
- {
- "name": "labelIds",
- "description": "The IDs of labels to be added to the board",
+ "name": "id",
+ "description": "The board global ID.",
"type": {
- "kind": "LIST",
+ "kind": "NON_NULL",
"name": null,
"ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "LabelID",
- "ofType": null
- }
+ "kind": "SCALAR",
+ "name": "BoardID",
+ "ofType": null
}
},
"defaultValue": null
@@ -68805,7 +68701,7 @@
"fields": [
{
"name": "board",
- "description": "The board after mutation",
+ "description": "The board after mutation.",
"args": [
],
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 83155402703..789a1ba16e3 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3584,7 +3584,7 @@ Autogenerated return type of UpdateBoard.
| Field | Type | Description |
| ----- | ---- | ----------- |
-| `board` | Board | The board after mutation |
+| `board` | Board | The board after mutation. |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b1f458391ba..eab99092e6f 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1077,6 +1077,7 @@ POST /projects
| `mirror` **(STARTER)** | boolean | **{dotted-circle}** No | Enables pull mirroring in a project. |
| `name` | string | **{check-circle}** Yes (if path isn't provided) | The name of the new project. Equals path if not provided. |
| `namespace_id` | integer | **{dotted-circle}** No | Namespace for the new project (defaults to the current user's namespace). |
+| `operations_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged when all the discussions are resolved. |
| `only_allow_merge_if_pipeline_succeeds` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged with successful jobs. |
| `packages_enabled` | boolean | **{dotted-circle}** No | Enable or disable packages repository feature. |
@@ -1147,6 +1148,7 @@ POST /projects/user/:user_id
| `mirror` **(STARTER)** | boolean | **{dotted-circle}** No | Enables pull mirroring in a project. |
| `name` | string | **{check-circle}** Yes | The name of the new project. |
| `namespace_id` | integer | **{dotted-circle}** No | Namespace for the new project (defaults to the current user's namespace). |
+| `operations_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged when all the discussions are resolved. |
| `only_allow_merge_if_pipeline_succeeds` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged with successful jobs. |
| `packages_enabled` | boolean | **{dotted-circle}** No | Enable or disable packages repository feature. |
@@ -1222,6 +1224,7 @@ PUT /projects/:id
| `mirror_user_id` **(STARTER)** | integer | **{dotted-circle}** No | User responsible for all the activity surrounding a pull mirror event. _(admins only)_ |
| `mirror` **(STARTER)** | boolean | **{dotted-circle}** No | Enables pull mirroring in a project. |
| `name` | string | **{dotted-circle}** No | The name of the project. |
+| `operations_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged when all the discussions are resolved. |
| `only_allow_merge_if_pipeline_succeeds` | boolean | **{dotted-circle}** No | Set whether merge requests can only be merged with successful jobs. |
| `only_mirror_protected_branches` **(STARTER)** | boolean | **{dotted-circle}** No | Only mirror protected branches. |
diff --git a/doc/api/users.md b/doc/api/users.md
index 7a75b880dcd..f73e1829024 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -416,7 +416,7 @@ Parameters:
| `note` | No | Admin notes for this user |
| `organization` | No | Organization name |
| `password` | No | Password |
-| `private_profile` | No | User's profile is private - true, false (default), or null (will be converted to false) |
+| `private_profile` | No | User's profile is private - true, false (default), or null (is converted to false) |
| `projects_limit` | No | Number of projects user can create |
| `provider` | No | External provider name |
| `public_email` | No | The public email of the user |
@@ -458,7 +458,7 @@ Parameters:
| `note` | No | Admin notes for this user |
| `organization` | No | Organization name |
| `password` | No | Password |
-| `private_profile` | No | User's profile is private - true, false (default), or null (will be converted to false) |
+| `private_profile` | No | User's profile is private - true, false (default), or null (is converted to false) |
| `projects_limit` | No | Limit projects each user can create |
| `provider` | No | External provider name |
| `public_email` | No | The public email of the user |
diff --git a/doc/ci/yaml/img/ci_config_visualization_hover_v13_5.png b/doc/ci/yaml/img/ci_config_visualization_hover_v13_5.png
deleted file mode 100644
index e6c85bd39e4..00000000000
--- a/doc/ci/yaml/img/ci_config_visualization_hover_v13_5.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/yaml/img/ci_config_visualization_hover_v13_7.png b/doc/ci/yaml/img/ci_config_visualization_hover_v13_7.png
new file mode 100644
index 00000000000..9387fc6ccf4
--- /dev/null
+++ b/doc/ci/yaml/img/ci_config_visualization_hover_v13_7.png
Binary files differ
diff --git a/doc/ci/yaml/img/ci_config_visualization_v13_5.png b/doc/ci/yaml/img/ci_config_visualization_v13_5.png
deleted file mode 100644
index 0aee5cff7be..00000000000
--- a/doc/ci/yaml/img/ci_config_visualization_v13_5.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/yaml/img/ci_config_visualization_v13_7.png b/doc/ci/yaml/img/ci_config_visualization_v13_7.png
new file mode 100644
index 00000000000..ef2aa6fe9e9
--- /dev/null
+++ b/doc/ci/yaml/img/ci_config_visualization_v13_7.png
Binary files differ
diff --git a/doc/ci/yaml/visualization.md b/doc/ci/yaml/visualization.md
index 04ffd304080..59a92370c70 100644
--- a/doc/ci/yaml/visualization.md
+++ b/doc/ci/yaml/visualization.md
@@ -7,6 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Visualize your CI/CD configuration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241722) in GitLab 13.5.
+> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
@@ -15,16 +16,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
-To see a visualization of your `gitlab-ci.yml` configuration, navigate to any CI/CD
-configuration file and click on the `Visualization` tab. The visualization shows
-all stages and jobs. [`needs`](README.md#needs) relationships are displayed as lines
-connecting jobs together, showing the hierarchy of execution:
+To see a visualization of your `gitlab-ci.yml` configuration, navigate to **CI/CD > Editor**
+and select the `Visualization` tab. The visualization shows all stages and jobs.
+[`needs`](README.md#needs) relationships are displayed as lines connecting jobs together, showing the hierarchy of execution:
-![CI Configuration Visualization](img/ci_config_visualization_v13_5.png)
+![CI Config Visualization](img/ci_config_visualization_v13_7.png)
Hovering on a job highlights its `needs` relationships:
-![CI Configuration Visualization on hover](img/ci_config_visualization_hover_v13_5.png)
+![CI Config Visualization on hover](img/ci_config_visualization_hover_v13_7.png)
If the configuration does not have any `needs` relationships, then no lines are drawn because
each job depends only on the previous stage being completed successfully.
@@ -42,11 +42,11 @@ can enable it.
To enable it:
```ruby
-Feature.enable(:gitlab_ci_yml_preview)
+Feature.enable(:ci_config_visualization_tab)
```
To disable it:
```ruby
-Feature.disable(:gitlab_ci_yml_preview)
+Feature.disable(:ci_config_visualization_tab)
```
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index 24a5b0efecc..b2c93f16770 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -34,7 +34,7 @@ for a good example):
- `desc` for the method summary. You should pass it a block for additional
details such as:
- The GitLab version when the endpoint was added. If it is behind a feature flag, mention that instead: _This feature is gated by the :feature\_flag\_symbol feature flag._
- - If the endpoint is deprecated, and if so, when will it be removed
+ - If the endpoint is deprecated, and if so, its planned removal date
- `params` for the method parameters. This acts as description,
[validation, and coercion of the parameters](https://github.com/ruby-grape/grape#parameter-validation-and-coercion)
@@ -72,7 +72,7 @@ parent namespaces.
– <https://github.com/ruby-grape/grape#include-parent-namespaces>
-In most cases you will want to exclude parameters from the parent namespaces:
+In most cases you should exclude parameters from the parent namespaces:
```ruby
declared(params, include_parent_namespaces: false)
@@ -94,7 +94,7 @@ User.create(declared(params, include_parent_namespaces: false).to_h)
```
NOTE:
-`declared(params)` return a `Hashie::Mash` object, on which you will have to
+`declared(params)` return a `Hashie::Mash` object, on which you must
call `.to_h`.
But we can use `params[key]` directly when we access single elements.
@@ -109,7 +109,7 @@ Model.create(foo: params[:foo])
## Array types
With Grape v1.3+, Array types must be defined with a `coerce_with`
-block, or parameters will fail to validate when passed a string from an
+block, or parameters, fails to validate when passed a string from an
API request. See the [Grape upgrading
documentation](https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#ensure-that-array-types-have-explicit-coercions)
for more details.
@@ -140,7 +140,7 @@ before do
end
```
-With this change, a request to PUT `/test?user_ids` will cause Grape to
+With this change, a request to PUT `/test?user_ids` causes Grape to
pass `params` to be `{ user_ids: [] }`.
There is [an open issue in the Grape tracker](https://github.com/ruby-grape/grape/issues/2068)
@@ -148,7 +148,7 @@ to make this easier.
## Using HTTP status helpers
-For non-200 HTTP responses, use the provided helpers in `lib/api/helpers.rb` to ensure correct behavior (`not_found!`, `no_content!` etc.). These will `throw` inside Grape and abort the execution of your endpoint.
+For non-200 HTTP responses, use the provided helpers in `lib/api/helpers.rb` to ensure correct behavior (`not_found!`, `no_content!` etc.). These `throw` inside Grape and abort the execution of your endpoint.
For `DELETE` requests, you should also generally use the `destroy_conditionally!` helper which by default returns a `204 No Content` response on success, or a `412 Precondition Failed` response if the given `If-Unmodified-Since` header is out of range. This helper calls `#destroy` on the passed resource, but you can also implement a custom deletion method by passing a block.
@@ -249,7 +249,7 @@ In order to avoid N+1 problems that are common when returning collections
of records in an API endpoint, we should use eager loading.
A standard way to do this within the API is for models to implement a
-scope called `with_api_entity_associations` that will preload the
+scope called `with_api_entity_associations` that preloads the
associations and data returned in the API. An example of this scope can
be seen in
[the `Issue` model](https://gitlab.com/gitlab-org/gitlab/blob/2fedc47b97837ea08c3016cf2fb773a0300a4a25/app%2Fmodels%2Fissue.rb#L62).
@@ -259,7 +259,7 @@ In situations where the same model has multiple entities in the API
discretion with applying this scope. It may be that you optimize for the
most basic entity, with successive entities building upon that scope.
-The `with_api_entity_associations` scope will also [automatically preload
+The `with_api_entity_associations` scope also [automatically preloads
data](https://gitlab.com/gitlab-org/gitlab/blob/19f74903240e209736c7668132e6a5a735954e7c/app%2Fmodels%2Ftodo.rb#L34)
for `Todo` _targets_ when returned in the [to-dos API](../api/todos.md).
diff --git a/doc/development/cached_queries.md b/doc/development/cached_queries.md
index 01c8e63edca..4f82d721164 100644
--- a/doc/development/cached_queries.md
+++ b/doc/development/cached_queries.md
@@ -29,7 +29,7 @@ more expensive from a memory perspective. They could mask
so you should treat them the same way you treat regular N+1 queries.
In cases of N+1 queries masked by cached queries, the same query is executed N times.
-It will not hit the database N times but instead returns the cached results N times.
+It doesn't hit the database N times but instead returns the cached results N times.
This is still expensive because you need to re-initialize objects each time at a
greater expense to the CPU and memory resources. Instead, you should use the same
in-memory objects whenever possible.
diff --git a/doc/development/deprecation_guidelines/index.md b/doc/development/deprecation_guidelines/index.md
index e1466074caf..c2fea3c6053 100644
--- a/doc/development/deprecation_guidelines/index.md
+++ b/doc/development/deprecation_guidelines/index.md
@@ -15,7 +15,7 @@ It's important to understand the difference between **deprecation** and
**removal**:
**Deprecation** is the process of flagging/marking/announcing that a feature
-will be removed in a future version of GitLab.
+is scheduled for removal in a future version of GitLab.
**Removal** is the process of actually removing a feature that was previously
deprecated.
diff --git a/doc/development/diffs.md b/doc/development/diffs.md
index 2f5f3fa21b6..5be7eafd1e7 100644
--- a/doc/development/diffs.md
+++ b/doc/development/diffs.md
@@ -79,7 +79,7 @@ File diffs are collapsed (but are expandable) if 100 files have already been ren
Gitlab::Git::DiffCollection.collection_limits[:safe_max_lines] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000
```
-File diffs will be collapsed (but be expandable) if 5000 lines have already been rendered.
+File diffs are collapsed (but be expandable) if 5000 lines have already been rendered.
```ruby
Gitlab::Git::DiffCollection.collection_limits[:safe_max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] * 5.kilobytes = 500.kilobytes
diff --git a/doc/development/emails.md b/doc/development/emails.md
index ded7cda78a4..0b1f3c5b74c 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -83,7 +83,7 @@ See the [Rails guides](https://guides.rubyonrails.org/action_mailer_basics.html#
expunge_deleted: false
```
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
+ As mentioned, the part after `+` is ignored, and this message is sent to the mailbox for `gitlab-incoming@gmail.com`.
1. Run this command in the GitLab root directory to launch `mail_room`:
diff --git a/doc/development/fe_guide/security.md b/doc/development/fe_guide/security.md
index ffc5f23f620..627c5f4d12f 100644
--- a/doc/development/fe_guide/security.md
+++ b/doc/development/fe_guide/security.md
@@ -73,7 +73,7 @@ such as with reCAPTCHA, which cannot be used without an `iframe`.
## Avoiding inline scripts and styles
-In order to protect users from [XSS vulnerabilities](https://en.wikipedia.org/wiki/Cross-site_scripting), we will disable
+In order to protect users from [XSS vulnerabilities](https://en.wikipedia.org/wiki/Cross-site_scripting), we intend to disable
inline scripts in the future using Content Security Policy.
While inline scripts can be useful, they're also a security concern. If
diff --git a/doc/development/multi_version_compatibility.md b/doc/development/multi_version_compatibility.md
index 5d5cbb2fe8f..25f4b3b5699 100644
--- a/doc/development/multi_version_compatibility.md
+++ b/doc/development/multi_version_compatibility.md
@@ -89,7 +89,7 @@ Puma enqueues jobs with an extra parameter that the old Sidekiq cannot handle.
### Database migrations
-The following graph is a simplified visual representation of a deployment, this will guide us in understanding how expand and contract is implemented in our migrations strategy.
+The following graph is a simplified visual representation of a deployment, this guides us in understanding how expand and contract is implemented in our migrations strategy.
There's a special consideration here. Using our post-deployment migrations framework allows us to bundle all three phases into one milestone.
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index 0644c0a62ed..3cc7b140e89 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -10,7 +10,7 @@ QueryRecorder is a tool for detecting the [N+1 queries problem](https://guides.r
> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/support/helpers/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-foss/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
-As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed will silently reintroduce the problem.
+As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed can silently reintroduce the problem.
## How it works
diff --git a/doc/development/renaming_features.md b/doc/development/renaming_features.md
index 2a94fd2cbc3..f7fc1c11e37 100644
--- a/doc/development/renaming_features.md
+++ b/doc/development/renaming_features.md
@@ -22,9 +22,9 @@ The more of the following that are true, the more likely you should choose the f
- You are not confident the new name is permanent.
- The feature is susceptible to bugs (large, complex, needing refactor, etc).
-- The renaming will be difficult to review (feature spans many lines, files, or repositories).
-- The renaming will be disruptive in some way (database table renaming).
+- The renaming is difficult to review (feature spans many lines, files, or repositories).
+- The renaming is disruptive in some way (database table renaming).
## Consider a façade-first approach
-The façade approach is not necessarily a final step. It can (and possibly *should*) be treated as the first step, where later iterations will accomplish the complete rename.
+The façade approach is not necessarily a final step. It can (and possibly *should*) be treated as the first step, where later iterations accomplish the complete rename.
diff --git a/doc/development/shared_files.md b/doc/development/shared_files.md
index 9ce13010141..859650c2e6c 100644
--- a/doc/development/shared_files.md
+++ b/doc/development/shared_files.md
@@ -12,7 +12,7 @@ etc. Having so many shared directories makes it difficult to deploy GitLab on
shared storage (e.g. NFS). Working towards GitLab 9.0 we are consolidating
these different directories under the `shared` directory.
-This means that if GitLab will start storing puppies in some future version
+This means that if GitLab begins storing puppies in some future version
then we should put them in `shared/puppies`. Temporary puppy files should be
stored in `shared/tmp`.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 9b3477e42c8..cc715034db0 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -268,10 +268,10 @@ You can exclude specific directories from the backup by adding the environment v
- `pages` (Pages content)
- `repositories` (Git repositories data)
-All wikis will be backed up as part of the `repositories` group. Non-existent wikis will be skipped during a backup.
-
+All wikis are backed up as part of the `repositories` group. Non-existent wikis are skipped during a backup.
+
NOTE:
-When [backing up and restoring Helm Charts](https://docs.gitlab.com/charts/architecture/backup-restore.html), there is an additional option `packages`, which refers to any packages managed by the GitLab [package registry](../user/packages/package_registry/index.md).
+When [backing up and restoring Helm Charts](https://docs.gitlab.com/charts/architecture/backup-restore.html), there is an additional option `packages`, which refers to any packages managed by the GitLab [package registry](../user/packages/package_registry/index.md).
For more information see [command line arguments](https://docs.gitlab.com/charts/architecture/backup-restore.html#command-line-arguments).
All wikis are backed up as part of the `repositories` group. Non-existent
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 0eefef4d37e..f09a5d9c69b 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -167,7 +167,7 @@ The following table depicts the various user permission levels in a project.
| Reposition comments on images (posted by any user)|✓ (*11*) | ✓ (*11*) | ✓ (*11*) | ✓ | ✓ |
| Manage Error Tracking | | | | ✓ | ✓ |
| Delete wiki pages | | | | ✓ | ✓ |
-| View project Audit Events | | | | ✓ | ✓ |
+| View project Audit Events | | | ✓ (*12*) | ✓ | ✓ |
| Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ |
| Manage [project access tokens](project/settings/project_access_tokens.md) **(CORE ONLY)** | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
@@ -195,6 +195,7 @@ The following table depicts the various user permission levels in a project.
[Eligible approvers](project/merge_requests/merge_request_approvals.md#eligible-approvers).
1. Owner permission is only available at the group or personal namespace level (and for instance admins) and is inherited by its projects.
1. Applies only to comments on [Design Management](project/issues/design_management.md) designs.
+1. Users can only view events based on their individual actions.
## Project features permissions
@@ -279,7 +280,7 @@ group.
| Delete group | | | | | ✓ |
| Delete group epic **(PREMIUM)** | | | | | ✓ |
| Edit SAML SSO Billing **(SILVER ONLY)** | ✓ | ✓ | ✓ | ✓ | ✓ (4) |
-| View group Audit Events | | | | | ✓ |
+| View group Audit Events | | | ✓ (7) | ✓ (7) | ✓ |
| Disable notification emails | | | | | ✓ |
| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
@@ -298,6 +299,7 @@ group.
1. Does not apply to subgroups.
1. Developers can push commits to the default branch of a new project only if the [default branch protection](group/index.md#changing-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected".
1. In addition, if your group is public or internal, all users who can see the group can also see group wiki pages.
+1. Users can only view events based on their individual actions.
### Subgroup permissions
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 23bd112a636..85cb139c45b 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -96,7 +96,7 @@ invitation, change their access level, or even delete them.
![Invite user members list](img/add_user_email_accept.png)
-While unaccepted, the system will automatically send reminder emails on the second, fifth,
+While unaccepted, the system automatically sends reminder emails on the second, fifth,
and tenth day after the invitation was initially sent.
After the user accepts the invitation, they are prompted to create a new
diff --git a/doc/user/project/merge_requests/getting_started.md b/doc/user/project/merge_requests/getting_started.md
index 530b75d45bf..cb95daa2cab 100644
--- a/doc/user/project/merge_requests/getting_started.md
+++ b/doc/user/project/merge_requests/getting_started.md
@@ -114,10 +114,11 @@ It is also possible to manage multiple assignees:
### Reviewer
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216054) in GitLab 13.5.
-> - It's [deployed behind a feature flag](../../../user/feature_flags.md), enabled by default.
-> - It's disabled on GitLab.com.
-> - It's not recommended for production use.
-> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-merge-request-reviewers). **(CORE ONLY)**
+> - It was [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
+> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49787) on GitLab 13.7.1.
+> - It's enabled on GitLab.com.
+> - It's recommended for production use.
+> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-merge-request-reviewers). **(CORE ONLY)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
@@ -136,21 +137,27 @@ To request it, open the **Reviewers** drop-down box to search for the user you w
#### Enable or disable Merge Request Reviewers **(CORE ONLY)**
-Merge Request Reviewers is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
+Merge Request Reviewers is under development but ready for production use.
+It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
-can enable it.
+can opt to disable it.
To enable it:
```ruby
+# For the instance
Feature.enable(:merge_request_reviewers)
+# For a single project
+Feature.enable(:merge_request_reviewers, Project.find(<project id>))
```
To disable it:
```ruby
+# For the instance
Feature.disable(:merge_request_reviewers)
+# For a single project
+Feature.disable(:merge_request_reviewers, Project.find(<project id>))
```
### Merge requests to close issues
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index fafd00e5a2f..01de98edeac 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -155,7 +155,7 @@ To add or edit the default merge request approval rule:
1. Click **Add approval rule**, or **Edit**.
- Add or change the **Rule name**.
- - Set the number of required approvals in **No. approvals required**. The minimum value is `0`.
+ - Set the number of required approvals in **Approvals required**. The minimum value is `0`.
- (Optional) Search for users or groups that will be [eligible to approve](#eligible-approvers)
merge requests and click the **Add** button to add them as approvers. Before typing
in the search field, approvers will be suggested based on the previous authors of
@@ -191,7 +191,7 @@ the same steps as [Adding / editing a default approval rule](#adding--editing-a-
MR approvals can be configured to be optional.
This can be useful if you're working on a team where approvals are appreciated, but not required.
-To configure an approval to be optional, set the number of required approvals in **No. approvals required** to `0`.
+To configure an approval to be optional, set the number of required approvals in **Approvals required** to `0`.
You can also set an optional approval rule through the [Merge requests approvals API](../../../api/merge_request_approvals.md#update-merge-request-level-rule), by setting the `approvals_required` attribute to `0`.
@@ -273,7 +273,7 @@ Regardless of the approval rules you choose for your project, users can edit the
request, overriding the rules you set as [default](#adding--editing-a-default-approval-rule).
To prevent that from happening:
-1. Uncheck the **Can override approvers and approvals required per merge request** checkbox.
+1. Uncheck the **Allow overrides to approval lists per merge request (MR).** checkbox.
1. Click **Save changes**.
#### Resetting approvals on push
@@ -282,7 +282,7 @@ You can force all approvals on a merge request to be removed when new commits ar
pushed to the source branch of the merge request. If disabled, approvals will persist
even if there are changes added to the merge request. To enable this feature:
-1. Check the **Remove all approvals in a merge request when new commits are pushed to its source branch**
+1. Check the **Require new approvals when new commits are added to an MR.**
checkbox.
1. Click **Save changes**.
@@ -298,7 +298,7 @@ By default, projects are configured to prevent merge requests from being approve
their own authors. To change this setting:
1. Go to your project's **Settings > General**, expand **Merge request approvals**.
-1. Uncheck the **Prevent approval of merge requests by merge request author** checkbox.
+1. Uncheck the **Prevent MR approval by the author.** checkbox.
1. Click **Save changes**.
Note that users can edit the approval rules in every merge request and override pre-defined settings unless it's set [**not to allow** overrides](#prevent-overriding-default-approvals).
@@ -310,7 +310,7 @@ Note that users can edit the approval rules in every merge request and override
You can prevent users that have committed to a merge request from approving it. To
enable this feature:
-1. Check the **Prevent approval of merge requests by their committers** checkbox.
+1. Check the **Prevent MR approvals from users who make commits to the MR.** checkbox.
1. Click **Save changes**.
#### Require authentication when approving a merge request
@@ -327,7 +327,7 @@ the approval. This enables an Electronic Signature for approvals such as the one
by [CFR Part 11](https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfcfr/CFRSearch.cfm?CFRPart=11&showFR=1&subpartNode=21:1.0.1.1.8.3)).
To enable this feature:
-1. Check the **Require user password to approve** checkbox.
+1. Check the **Require user password for approvals.** checkbox.
1. Click **Save changes**.
### Security approvals in merge requests **(ULTIMATE)**
diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md
index a484d71d0e1..9d75c4ab071 100644
--- a/doc/user/project/requirements/index.md
+++ b/doc/user/project/requirements/index.md
@@ -30,9 +30,11 @@ For an overview, see [GitLab 12.10 Introduces Requirements Management](https://w
A paginated list of requirements is available in each project, and there you
can create a new requirement.
+Users with Reporter or higher [permissions](../../permissions.md) can create requirements.
+
To create a requirement:
-1. From your project page, go to **{requirements}** **Requirements**.
+1. From your project page, go to **Requirements**.
1. Select **New requirement**.
1. Enter a title and description and select **Create requirement**.
@@ -54,8 +56,9 @@ next to the requirement title.
> The ability to mark a requirement as Satisfied [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218607) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5.
-You can edit a requirement (if you have the necessary privileges) from the requirements
-list page.
+You can edit a requirement from the requirements list page.
+
+Users with Reporter or higher [permissions](../../permissions.md) can edit requirements.
To edit a requirement:
@@ -66,9 +69,11 @@ To edit a requirement:
## Archive a requirement
-You can archive an open requirement (if you have the necessary privileges) while
+You can archive an open requirement while
you're in the **Open** tab.
+Users with Reporter or higher [permissions](../../permissions.md) can archive requirements.
+
To archive a requirement, select **Archive** (**{archive}**).
As soon as a requirement is archived, it no longer appears in the **Open** tab.
@@ -77,6 +82,8 @@ As soon as a requirement is archived, it no longer appears in the **Open** tab.
You can view the list of archived requirements in the **Archived** tab.
+Users with Reporter or higher [permissions](../../permissions.md) can reopen archived requirements.
+
![archived requirements list](img/requirements_archived_list_view_v13_1.png)
To reopen an archived requirement, select **Reopen**.
@@ -94,7 +101,7 @@ You can search for a requirement from the requirements list page based on the fo
To search for a requirement:
-1. In a project, go to **{requirements}** **Requirements > List**.
+1. In a project, go to **Requirements > List**.
1. Select the **Search or filter results** field. A dropdown menu appears.
1. Select the requirement author from the dropdown or enter plain text to search by requirement title.
1. Press <kbd>Enter</kbd> on your keyboard to filter the list.
@@ -188,3 +195,65 @@ requirements_confirmation:
reports:
requirements: tmp/requirements.json
```
+
+## Import requirements from a CSV file
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/246857) in GitLab 13.7.
+
+You can import requirements to a project by uploading a CSV file with the columns
+`title` and `description`.
+
+The user uploading the CSV file will be set as the author of the imported requirements.
+
+Users with Reporter or higher [permissions](../../permissions.md) can import requirements.
+
+### Import the file
+
+Before you import your file:
+
+- Consider importing a test file containing only a few requirements. There is no way to undo a large
+ import without using the GitLab API.
+- Ensure your CSV file meets the [file format](#csv-file-format) requirements.
+
+To import requirements:
+
+1. Navigate to a project's Requirements page.
+ - If the project already has existing requirements, click the import icon (**{import}**) at the
+ top right.
+ - For a project without any requirements, click **Import CSV** in the middle of the page.
+1. Select the file and click **Import requirements**.
+
+The file is processed in the background and a notification email is sent
+to you after the import is complete.
+
+### CSV file format
+
+When importing requirements from a CSV file, it must be formatted in a certain way:
+
+- **Header row:** CSV files must include the following headers:
+ `title` and `description`. The headers are case insensitive.
+- **Columns:** data from columns other than `title` and `description` is not imported.
+- **Separators:** the column separator is automatically detected from the header row.
+ Supported separator characters are: commas (`,`), semicolons (`;`), and tabs (`\t`).
+ The row separator can be either `CRLF` or `LF`.
+- **Double-quote character:** the double-quote (`"`) character is used to quote fields,
+ enabling the use of the column separator in a field (see the third line in the
+ sample CSV data below). To insert a double-quote (`"`) in a quoted
+ field, use two double-quote characters in succession (`""`).
+- **Data rows:** below the header row, succeeding rows must follow the same column
+ order. The title text is required, while the description is optional and can be left empty.
+
+Sample CSV data:
+
+```plaintext
+title,description
+My Requirement Title,My Requirement Description
+Another Title,"A description, with a comma"
+"One More Title","One More Description"
+```
+
+### File size
+
+The limit depends on the configuration value of Max Attachment Size for the GitLab instance.
+
+For GitLab.com, it is set to 10 MB.
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index bb48712fee6..05968bb1f20 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -72,7 +72,8 @@ Use the switches to enable or disable the following features:
| **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md) |
| **Pages** | ✓ | Allows you to [publish static websites](../pages/) |
| **Metrics Dashboard** | ✓ | Control access to [metrics dashboard](../integrations/prometheus.md)
-| **Requirements** | ✓ | Control access to [Requirements Management](../requirements/index.md)
+| **Requirements** | ✓ | Control access to [Requirements Management](../requirements/index.md) |
+| **Operations Dashboard** | ✓ | Control access to [operations dashboard](../../../operations/index.md)
Some features depend on others:
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index 82a44c75382..e7610db85cb 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -67,6 +67,7 @@ module API
expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) }
expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) }
expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) }
+ expose(:operations_access_level) { |project, options| project.project_feature.string_access_level(:operations) }
expose :emails_disabled
expose :shared_runners_enabled
diff --git a/lib/api/features.rb b/lib/api/features.rb
index 2ab60e2617d..57bd7c38ad2 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -75,7 +75,6 @@ module API
post ':name' do
validate_feature_flag_name!(params[:name]) unless params[:force]
- feature = Feature.get(params[:name]) # rubocop:disable Gitlab/AvoidFeatureGet
targets = gate_targets(params)
value = gate_value(params)
key = gate_key(params)
@@ -83,25 +82,26 @@ module API
case value
when true
if gate_specified?(params)
- targets.each { |target| feature.enable(target) }
+ targets.each { |target| Feature.enable(params[:name], target) }
else
- feature.enable
+ Feature.enable(params[:name])
end
when false
if gate_specified?(params)
- targets.each { |target| feature.disable(target) }
+ targets.each { |target| Feature.disable(params[:name], target) }
else
- feature.disable
+ Feature.disable(params[:name])
end
else
if key == :percentage_of_actors
- feature.enable_percentage_of_actors(value)
+ Feature.enable_percentage_of_actors(params[:name], value)
else
- feature.enable_percentage_of_time(value)
+ Feature.enable_percentage_of_time(params[:name], value)
end
end
- present feature, with: Entities::Feature, current_user: current_user
+ present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet
+ with: Entities::Feature, current_user: current_user
end
desc 'Remove the gate value for the given feature'
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 2fdbbce8606..2a6f620ee86 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -32,6 +32,7 @@ module API
optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`'
optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`'
optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`'
+ optional :operations_access_level, type: String, values: %w(disabled private enabled), desc: 'Operations access level. One of `disabled`, `private` or `enabled`'
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b056d72bb11..e15354baf02 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2880,6 +2880,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow overrides to approval lists per merge request (MR)"
+msgstr ""
+
msgid "Allow owners to manage default branch protection per group"
msgstr ""
@@ -3441,7 +3444,7 @@ msgstr ""
msgid "Any label"
msgstr ""
-msgid "Any member with Developer or higher permissions to the project."
+msgid "Any member with at least Developer permissions on the project."
msgstr ""
msgid "Any milestone"
@@ -3590,13 +3593,13 @@ msgstr[1] ""
msgid "ApprovalRule|Approval rules"
msgstr ""
-msgid "ApprovalRule|Approvers"
+msgid "ApprovalRule|Approvals required"
msgstr ""
-msgid "ApprovalRule|Name"
+msgid "ApprovalRule|Approvers"
msgstr ""
-msgid "ApprovalRule|No. approvals required"
+msgid "ApprovalRule|Name"
msgstr ""
msgid "ApprovalRule|Rule name"
@@ -4963,9 +4966,6 @@ msgstr ""
msgid "Can be manually deployed to"
msgstr ""
-msgid "Can override approvers and approvals required per merge request"
-msgstr ""
-
msgid "Can't apply as the source branch was deleted."
msgstr ""
@@ -6910,9 +6910,6 @@ msgstr ""
msgid "Code Owners"
msgstr ""
-msgid "Code Owners to the merge request changes."
-msgstr ""
-
msgid "Code Quality"
msgstr ""
@@ -8962,6 +8959,9 @@ msgstr ""
msgid "Define a custom pattern with cron syntax"
msgstr ""
+msgid "Define approval settings."
+msgstr ""
+
msgid "Define custom rules for what constitutes spam, independent of Akismet"
msgstr ""
@@ -12224,12 +12224,6 @@ msgstr ""
msgid "Filter by name"
msgstr ""
-msgid "Filter by requirements that are currently archived."
-msgstr ""
-
-msgid "Filter by requirements that are currently opened."
-msgstr ""
-
msgid "Filter by status"
msgstr ""
@@ -16238,9 +16232,6 @@ msgstr ""
msgid "Learn more about adding certificates to your project by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}."
msgstr ""
-msgid "Learn more about approvals."
-msgstr ""
-
msgid "Learn more about custom project templates"
msgstr ""
@@ -16265,6 +16256,9 @@ msgstr ""
msgid "Learn more in the|pipeline schedules documentation"
msgstr ""
+msgid "Learn more."
+msgstr ""
+
msgid "Leave"
msgstr ""
@@ -17098,6 +17092,9 @@ msgstr ""
msgid "Members invited to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
+msgid "Members listed as CODEOWNERS of affected files."
+msgstr ""
+
msgid "Members of %{group} can also merge into this branch: %{branch}"
msgstr ""
@@ -20947,6 +20944,12 @@ msgstr ""
msgid "Prev"
msgstr ""
+msgid "Prevent MR approvals by the author."
+msgstr ""
+
+msgid "Prevent MR approvals from users who make commits to the MR."
+msgstr ""
+
msgid "Prevent adding new members to project membership within this group"
msgstr ""
@@ -23054,9 +23057,6 @@ msgstr ""
msgid "Remove Zoom meeting"
msgstr ""
-msgid "Remove all approvals in a merge request when new commits are pushed to its source branch"
-msgstr ""
-
msgid "Remove all or specific assignee(s)"
msgstr ""
@@ -23597,7 +23597,10 @@ msgstr ""
msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr ""
-msgid "Require user password to approve"
+msgid "Require new approvals when new commits are added to an MR."
+msgstr ""
+
+msgid "Require user password for approvals."
msgstr ""
msgid "Require users to prove ownership of custom domains"
@@ -24461,16 +24464,16 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "SecurityApprovals|License Scanning must be enabled. %{linkStart}More information%{linkEnd}"
+msgid "SecurityApprovals|Configurable if security scanners are enabled. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
-msgid "SecurityApprovals|One or more of the security scanners must be enabled. %{linkStart}More information%{linkEnd}"
+msgid "SecurityApprovals|License Scanning must be enabled. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "SecurityApprovals|Requires approval for Denied licenses. %{linkStart}More information%{linkEnd}"
msgstr ""
-msgid "SecurityApprovals|Requires approval for vulnerabilities of Critical, High, or Unknown severity. %{linkStart}More information%{linkEnd}"
+msgid "SecurityApprovals|Requires approval for vulnerabilities of Critical, High, or Unknown severity. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityConfiguration|An error occurred while creating the merge request."
@@ -25174,9 +25177,6 @@ msgstr ""
msgid "Set a default template for issue descriptions."
msgstr ""
-msgid "Set a number of approvals required, the approvers and other approval settings."
-msgstr ""
-
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr ""
@@ -25450,9 +25450,6 @@ msgstr ""
msgid "Show all members"
msgstr ""
-msgid "Show all requirements."
-msgstr ""
-
msgid "Show all test cases."
msgstr ""
@@ -31243,7 +31240,7 @@ msgstr ""
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "Who can be an approver?"
+msgid "Who can approve?"
msgstr ""
msgid "Who can see this group?"
@@ -32062,6 +32059,9 @@ msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CI configuration file is invalid."
+msgstr ""
+
msgid "Your CSV export has started. It will be emailed to %{email} when complete."
msgstr ""
diff --git a/package.json b/package.json
index 5361bddd813..14c3fd8533e 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,7 @@
"deckar01-task_list": "^2.3.1",
"diff": "^3.4.0",
"document-register-element": "1.14.3",
- "dompurify": "^2.2.3",
+ "dompurify": "^2.2.4",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^7.0.3",
diff --git a/rubocop/cop/rspec/httparty_basic_auth.rb b/rubocop/cop/rspec/httparty_basic_auth.rb
new file mode 100644
index 00000000000..529a5808662
--- /dev/null
+++ b/rubocop/cop/rspec/httparty_basic_auth.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module RSpec
+ # This cop checks for invalid credentials passed to HTTParty
+ #
+ # @example
+ #
+ # # bad
+ # HTTParty.get(url, basic_auth: { user: 'foo' })
+ #
+ # # good
+ # HTTParty.get(url, basic_auth: { username: 'foo' })
+ class HTTPartyBasicAuth < RuboCop::Cop::Cop
+ MESSAGE = "`basic_auth: { user: ... }` does not work - replace `user:` with `username:`".freeze
+
+ RESTRICT_ON_SEND = %i(get put post delete).freeze
+
+ def_node_matcher :httparty_basic_auth?, <<~PATTERN
+ (send
+ (const _ :HTTParty)
+ {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}}
+ <(hash
+ <(pair
+ (sym :basic_auth)
+ (hash
+ <(pair $(sym :user) _) ...>
+ )
+ ) ...>
+ ) ...>
+ )
+ PATTERN
+
+ def on_send(node)
+ return unless m = httparty_basic_auth?(node)
+
+ add_offense(m, location: :expression, message: MESSAGE)
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ corrector.replace(node.loc.expression, 'username')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/file_uploads/git_lfs_spec.rb b/spec/features/file_uploads/git_lfs_spec.rb
index 3824c04ada5..239afb1a1bb 100644
--- a/spec/features/file_uploads/git_lfs_spec.rb
+++ b/spec/features/file_uploads/git_lfs_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Upload a git lfs object', :js do
HTTParty.put(
url,
headers: headers,
- basic_auth: { user: user.username, password: personal_access_token.token },
+ basic_auth: { username: user.username, password: personal_access_token.token },
body: file.read
)
end
diff --git a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
index b3ace2e30ff..91c8e100e6a 100644
--- a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
+++ b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
subject do
HTTParty.put(
url,
- basic_auth: { user: user.username, password: personal_access_token.token },
+ basic_auth: { username: user.username, password: personal_access_token.token },
body: body
)
end
diff --git a/spec/features/file_uploads/nuget_package_spec.rb b/spec/features/file_uploads/nuget_package_spec.rb
index fb1e0a54744..6e05e5d1a6e 100644
--- a/spec/features/file_uploads/nuget_package_spec.rb
+++ b/spec/features/file_uploads/nuget_package_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Upload a nuget package', :api, :js do
subject do
HTTParty.put(
url,
- basic_auth: { user: user.username, password: personal_access_token.token },
+ basic_auth: { username: user.username, password: personal_access_token.token },
body: { package: file }
)
end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index dec07eb3783..a4c450c9a2c 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -33,6 +33,7 @@ RSpec.describe 'Group navbar' do
nav_item: _('Merge Requests'),
nav_sub_items: []
},
+ (security_and_compliance_nav_item if Gitlab.ee?),
(push_rules_nav_item if Gitlab.ee?),
{
nav_item: _('Kubernetes'),
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index ca54c97d2bb..14d6b03645c 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -51,9 +51,15 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
const createComponent = ({
props = {},
- loading = false,
+ blobLoading = false,
+ lintLoading = false,
options = {},
mountFn = shallowMount,
+ provide = {
+ glFeatures: {
+ ciConfigVisualizationTab: true,
+ },
+ },
} = {}) => {
mockMutate = jest.fn().mockResolvedValue({
data: {
@@ -73,6 +79,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
newMergeRequestPath: mockNewMergeRequestPath,
...props,
},
+ provide,
stubs: {
GlTabs,
GlButton,
@@ -86,7 +93,10 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
$apollo: {
queries: {
content: {
- loading,
+ loading: blobLoading,
+ },
+ ciConfigData: {
+ loading: lintLoading,
},
},
mutate: mockMutate,
@@ -124,9 +134,12 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAlert = () => wrapper.find(GlAlert);
+ const findBlobFailureAlert = () => wrapper.find(GlAlert);
const findTabAt = i => wrapper.findAll(GlTab).at(i);
+ const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
const findTextEditor = () => wrapper.find(TextEditor);
const findCommitForm = () => wrapper.find(CommitForm);
+ const findPipelineGraph = () => wrapper.find(PipelineGraph);
const findCommitBtnLoadingIcon = () => wrapper.find('[type="submit"]').find(GlLoadingIcon);
beforeEach(() => {
@@ -145,39 +158,65 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
wrapper = null;
});
- it('displays a loading icon if the query is loading', () => {
- createComponent({ loading: true });
+ it('displays a loading icon if the blob query is loading', () => {
+ createComponent({ blobLoading: true });
expect(findLoadingIcon().exists()).toBe(true);
expect(findTextEditor().exists()).toBe(false);
});
describe('tabs', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('editor tab', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the tab and its content', async () => {
+ expect(
+ findTabAt(0)
+ .find(TextEditor)
+ .exists(),
+ ).toBe(true);
+ });
- it('displays tabs and their content', async () => {
- expect(
- findTabAt(0)
- .find(TextEditor)
- .exists(),
- ).toBe(true);
- expect(
- findTabAt(1)
- .find(PipelineGraph)
- .exists(),
- ).toBe(true);
+ it('displays tab lazily, until editor is ready', async () => {
+ expect(findTabAt(0).attributes('lazy')).toBe('true');
+
+ findTextEditor().vm.$emit('editor-ready');
+
+ await nextTick();
+
+ expect(findTabAt(0).attributes('lazy')).toBe(undefined);
+ });
});
- it('displays editor tab lazily, until editor is ready', async () => {
- expect(findTabAt(0).attributes('lazy')).toBe('true');
+ describe('visualization tab', () => {
+ describe('with feature flag on', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- findTextEditor().vm.$emit('editor-ready');
+ it('display the tab', () => {
+ expect(findVisualizationTab().exists()).toBe(true);
+ });
+
+ it('displays a loading icon if the lint query is loading', () => {
+ createComponent({ lintLoading: true });
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
+ });
- await nextTick();
+ describe('with feature flag off', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { ciConfigVisualizationTab: false } } });
+ });
- expect(findTabAt(0).attributes('lazy')).toBe(undefined);
+ it('does not display the tab', () => {
+ expect(findVisualizationTab().exists()).toBe(false);
+ });
+ });
});
});
@@ -359,7 +398,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
- expect(findAlert().exists()).toBe(false);
+ expect(findBlobFailureAlert().exists()).toBe(false);
expect(findTextEditor().attributes('value')).toBe(mockCiYml);
});
@@ -373,7 +412,9 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
- expect(findAlert().text()).toMatch('No CI file found in this repository, please add one.');
+ expect(findBlobFailureAlert().text()).toBe(
+ 'No CI file found in this repository, please add one.',
+ );
});
it('shows a 400 error message', async () => {
@@ -386,7 +427,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
- expect(findAlert().text()).toMatch(
+ expect(findBlobFailureAlert().text()).toBe(
'Repository does not have a default branch, please set one.',
);
});
@@ -396,7 +437,9 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
createComponentWithApollo();
await waitForPromises();
- expect(findAlert().text()).toMatch('The CI configuration was not loaded, please try again.');
+ expect(findBlobFailureAlert().text()).toBe(
+ 'The CI configuration was not loaded, please try again.',
+ );
});
});
});
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
index 4f55fdd6b28..a77973b293c 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -1,4 +1,4 @@
-import { createUniqueJobId } from '~/pipelines/utils';
+import { createUniqueLinkId } from '~/pipelines/utils';
export const yamlString = `stages:
- empty
@@ -41,10 +41,10 @@ deploy_a:
script: echo hello
`;
-const jobId1 = createUniqueJobId('build', 'build_1');
-const jobId2 = createUniqueJobId('test', 'test_1');
-const jobId3 = createUniqueJobId('test', 'test_2');
-const jobId4 = createUniqueJobId('deploy', 'deploy_1');
+const jobId1 = createUniqueLinkId('build', 'build_1');
+const jobId2 = createUniqueLinkId('test', 'test_1');
+const jobId3 = createUniqueLinkId('test', 'test_2');
+const jobId4 = createUniqueLinkId('deploy', 'deploy_1');
export const pipelineData = {
stages: [
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index 7c8ebc27974..6704ee06c1a 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,5 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
import { pipelineData, singleStageData } from './mock_data';
+import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
+import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
@@ -8,15 +11,16 @@ describe('pipeline graph component', () => {
const defaultProps = { pipelineData };
let wrapper;
- const createComponent = props => {
+ const createComponent = (props = defaultProps) => {
return shallowMount(PipelineGraph, {
propsData: {
- ...defaultProps,
...props,
},
});
};
+ const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
+ const findAlert = () => wrapper.find(GlAlert);
const findAllStagePills = () => wrapper.findAll(StagePill);
const findAllStageBackgroundElements = () => wrapper.findAll('[data-testid="stage-background"]');
const findStageBackgroundElementAt = index => findAllStageBackgroundElements().at(index);
@@ -33,54 +37,92 @@ describe('pipeline graph component', () => {
});
it('renders an empty section', () => {
- expect(wrapper.text()).toContain(
- 'The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax.',
- );
+ expect(wrapper.text()).toBe(wrapper.vm.$options.warningTexts[EMPTY_PIPELINE_DATA]);
+ expect(findPipelineGraph().exists()).toBe(false);
expect(findAllStagePills()).toHaveLength(0);
expect(findAllJobPills()).toHaveLength(0);
});
});
- describe('with data', () => {
+ describe('with `INVALID` status', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ pipelineData: { status: CI_CONFIG_STATUS_INVALID } });
+ });
+
+ it('renders an error message and does not render the graph', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.warningTexts[INVALID_CI_CONFIG]);
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
+ });
+
+ describe('without `INVALID` status', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('renders the graph with no status error', () => {
+ expect(findAlert().text()).not.toBe(wrapper.vm.$options.warningTexts[INVALID_CI_CONFIG]);
+ expect(findPipelineGraph().exists()).toBe(true);
+ });
+ });
+
+ describe('with error while rendering the links', () => {
beforeEach(() => {
wrapper = createComponent();
});
+ it('renders the error that link could not be drawn', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[DRAW_FAILURE]);
+ });
+ });
+
+ describe('with only one stage', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ pipelineData: singleStageData });
+ });
+
it('renders the right number of stage pills', () => {
- const expectedStagesLength = pipelineData.stages.length;
+ const expectedStagesLength = singleStageData.stages.length;
expect(findAllStagePills()).toHaveLength(expectedStagesLength);
});
- it.each`
- cssClass | expectedState
- ${'gl-rounded-bottom-left-6'} | ${true}
- ${'gl-rounded-top-left-6'} | ${true}
- ${'gl-rounded-top-right-6'} | ${false}
- ${'gl-rounded-bottom-right-6'} | ${false}
- `(
- 'rounds corner: $class should be $expectedState on the first element',
- ({ cssClass, expectedState }) => {
+ it('renders the right number of job pills', () => {
+ // We count the number of jobs in the mock data
+ const expectedJobsLength = singleStageData.stages.reduce((acc, val) => {
+ return acc + val.groups.length;
+ }, 0);
+
+ expect(findAllJobPills()).toHaveLength(expectedJobsLength);
+ });
+
+ describe('rounds corner', () => {
+ it.each`
+ cssClass | expectedState
+ ${'gl-rounded-bottom-left-6'} | ${true}
+ ${'gl-rounded-top-left-6'} | ${true}
+ ${'gl-rounded-top-right-6'} | ${true}
+ ${'gl-rounded-bottom-right-6'} | ${true}
+ `('$cssClass should be $expectedState on the only element', ({ cssClass, expectedState }) => {
const classes = findStageBackgroundElementAt(0).classes();
expect(classes.includes(cssClass)).toBe(expectedState);
- },
- );
-
- it.each`
- cssClass | expectedState
- ${'gl-rounded-bottom-left-6'} | ${false}
- ${'gl-rounded-top-left-6'} | ${false}
- ${'gl-rounded-top-right-6'} | ${true}
- ${'gl-rounded-bottom-right-6'} | ${true}
- `(
- 'rounds corner: $class should be $expectedState on the last element',
- ({ cssClass, expectedState }) => {
- const classes = findStageBackgroundElementAt(pipelineData.stages.length - 1).classes();
+ });
+ });
+ });
- expect(classes.includes(cssClass)).toBe(expectedState);
- },
- );
+ describe('with multiple stages and jobs', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('renders the right number of stage pills', () => {
+ const expectedStagesLength = pipelineData.stages.length;
+
+ expect(findAllStagePills()).toHaveLength(expectedStagesLength);
+ });
it('renders the right number of job pills', () => {
// We count the number of jobs in the mock data
@@ -90,26 +132,34 @@ describe('pipeline graph component', () => {
expect(findAllJobPills()).toHaveLength(expectedJobsLength);
});
- });
- describe('with only one stage', () => {
- beforeEach(() => {
- wrapper = createComponent({ pipelineData: singleStageData });
- });
+ describe('rounds corner', () => {
+ it.each`
+ cssClass | expectedState
+ ${'gl-rounded-bottom-left-6'} | ${true}
+ ${'gl-rounded-top-left-6'} | ${true}
+ ${'gl-rounded-top-right-6'} | ${false}
+ ${'gl-rounded-bottom-right-6'} | ${false}
+ `(
+ '$cssClass should be $expectedState on the first element',
+ ({ cssClass, expectedState }) => {
+ const classes = findStageBackgroundElementAt(0).classes();
+
+ expect(classes.includes(cssClass)).toBe(expectedState);
+ },
+ );
- it.each`
- cssClass | expectedState
- ${'gl-rounded-bottom-left-6'} | ${true}
- ${'gl-rounded-top-left-6'} | ${true}
- ${'gl-rounded-top-right-6'} | ${true}
- ${'gl-rounded-bottom-right-6'} | ${true}
- `(
- 'rounds corner: $class should be $expectedState on the only element',
- ({ cssClass, expectedState }) => {
- const classes = findStageBackgroundElementAt(0).classes();
+ it.each`
+ cssClass | expectedState
+ ${'gl-rounded-bottom-left-6'} | ${false}
+ ${'gl-rounded-top-left-6'} | ${false}
+ ${'gl-rounded-top-right-6'} | ${true}
+ ${'gl-rounded-bottom-right-6'} | ${true}
+ `('$cssClass should be $expectedState on the last element', ({ cssClass, expectedState }) => {
+ const classes = findStageBackgroundElementAt(pipelineData.stages.length - 1).classes();
expect(classes.includes(cssClass)).toBe(expectedState);
- },
- );
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
index b073ad7647e..12154df6fcf 100644
--- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
@@ -1,14 +1,24 @@
-import { createUniqueJobId, generateJobNeedsDict } from '~/pipelines/utils';
+import { createJobsHash, generateJobNeedsDict } from '~/pipelines/utils';
describe('utils functions', () => {
const jobName1 = 'build_1';
const jobName2 = 'build_2';
const jobName3 = 'test_1';
const jobName4 = 'deploy_1';
- const job1 = { script: 'echo hello', stage: 'build' };
- const job2 = { script: 'echo build', stage: 'build' };
- const job3 = { script: 'echo test', stage: 'test', needs: [jobName1, jobName2] };
- const job4 = { script: 'echo deploy', stage: 'deploy', needs: [jobName3] };
+ const job1 = { name: jobName1, script: 'echo hello', stage: 'build' };
+ const job2 = { name: jobName2, script: 'echo build', stage: 'build' };
+ const job3 = {
+ name: jobName3,
+ script: 'echo test',
+ stage: 'test',
+ needs: [jobName1, jobName2],
+ };
+ const job4 = {
+ name: jobName4,
+ script: 'echo deploy',
+ stage: 'deploy',
+ needs: [jobName3],
+ };
const userDefinedStage = 'myStage';
const pipelineGraphData = {
@@ -23,7 +33,6 @@ describe('utils functions', () => {
{
name: jobName4,
jobs: [{ ...job4 }],
- id: createUniqueJobId(job4.stage, jobName4),
},
],
},
@@ -33,12 +42,10 @@ describe('utils functions', () => {
{
name: jobName1,
jobs: [{ ...job1 }],
- id: createUniqueJobId(job1.stage, jobName1),
},
{
name: jobName2,
jobs: [{ ...job2 }],
- id: createUniqueJobId(job2.stage, jobName2),
},
],
},
@@ -48,49 +55,59 @@ describe('utils functions', () => {
{
name: jobName3,
jobs: [{ ...job3 }],
- id: createUniqueJobId(job3.stage, jobName3),
},
],
},
],
- jobs: {
- [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
- [jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
- [jobName3]: { ...job3, id: createUniqueJobId(job3.stage, jobName3) },
- [jobName4]: { ...job4, id: createUniqueJobId(job4.stage, jobName4) },
- },
};
+ describe('createJobsHash', () => {
+ it('returns an empty object if there are no jobs received as argument', () => {
+ expect(createJobsHash([])).toEqual({});
+ });
+
+ it('returns a hash with the jobname as key and all its data as value', () => {
+ const jobs = {
+ [jobName1]: job1,
+ [jobName2]: job2,
+ [jobName3]: job3,
+ [jobName4]: job4,
+ };
+
+ expect(createJobsHash(pipelineGraphData.stages)).toEqual(jobs);
+ });
+ });
+
describe('generateJobNeedsDict', () => {
it('generates an empty object if it receives no jobs', () => {
- expect(generateJobNeedsDict({ jobs: {} })).toEqual({});
+ expect(generateJobNeedsDict({})).toEqual({});
});
it('generates a dict with empty needs if there are no dependencies', () => {
const smallGraph = {
- jobs: {
- [jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
- [jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
- },
+ [jobName1]: job1,
+ [jobName2]: job2,
};
expect(generateJobNeedsDict(smallGraph)).toEqual({
- [pipelineGraphData.jobs[jobName1].id]: [],
- [pipelineGraphData.jobs[jobName2].id]: [],
+ [jobName1]: [],
+ [jobName2]: [],
});
});
it('generates a dict where key is the a job and its value is an array of all its needs', () => {
- const uniqueJobName1 = pipelineGraphData.jobs[jobName1].id;
- const uniqueJobName2 = pipelineGraphData.jobs[jobName2].id;
- const uniqueJobName3 = pipelineGraphData.jobs[jobName3].id;
- const uniqueJobName4 = pipelineGraphData.jobs[jobName4].id;
+ const jobsWithNeeds = {
+ [jobName1]: job1,
+ [jobName2]: job2,
+ [jobName3]: job3,
+ [jobName4]: job4,
+ };
- expect(generateJobNeedsDict(pipelineGraphData)).toEqual({
- [uniqueJobName1]: [],
- [uniqueJobName2]: [],
- [uniqueJobName3]: [uniqueJobName1, uniqueJobName2],
- [uniqueJobName4]: [uniqueJobName3, uniqueJobName1, uniqueJobName2],
+ expect(generateJobNeedsDict(jobsWithNeeds)).toEqual({
+ [jobName1]: [],
+ [jobName2]: [],
+ [jobName3]: [jobName1, jobName2],
+ [jobName4]: [jobName3, jobName1, jobName2],
});
});
});
diff --git a/spec/frontend/pipelines/unwrapping_utils_spec.js b/spec/frontend/pipelines/unwrapping_utils_spec.js
index 34413ad3ef3..3533599611f 100644
--- a/spec/frontend/pipelines/unwrapping_utils_spec.js
+++ b/spec/frontend/pipelines/unwrapping_utils_spec.js
@@ -1,4 +1,5 @@
import {
+ unwrapArrayOfJobs,
unwrapGroups,
unwrapNodesWithName,
unwrapStagesWithNeeds,
@@ -94,6 +95,29 @@ const completeMock = [
];
describe('Shared pipeline unwrapping utils', () => {
+ describe('unwrapArrayOfJobs', () => {
+ it('returns an empty array if the input is an empty undefined', () => {
+ expect(unwrapArrayOfJobs(undefined)).toEqual([]);
+ });
+
+ it('returns an empty array if the input is an empty array', () => {
+ expect(unwrapArrayOfJobs([])).toEqual([]);
+ });
+
+ it('returns a flatten array of each job with their data and stage name', () => {
+ expect(
+ unwrapArrayOfJobs([
+ { name: 'build', groups: [{ name: 'job_a_1' }, { name: 'job_a_2' }] },
+ { name: 'test', groups: [{ name: 'job_b' }] },
+ ]),
+ ).toMatchObject([
+ { category: 'build', name: 'job_a_1' },
+ { category: 'build', name: 'job_a_2' },
+ { category: 'test', name: 'job_b' },
+ ]);
+ });
+ });
+
describe('unwrapGroups', () => {
it('takes stages without nodes and returns the unwrapped groups', () => {
expect(unwrapGroups(stagesAndGroups)[0].groups).toEqual(groupsArray);
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index e67e8cf3dff..d28d5ecda1b 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -416,6 +416,7 @@ RSpec.describe ProjectsHelper do
describe '#get_project_nav_tabs' do
before do
+ allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?) { true }
end
diff --git a/spec/models/label_priority_spec.rb b/spec/models/label_priority_spec.rb
index db961d5a4e6..adeccd999f3 100644
--- a/spec/models/label_priority_spec.rb
+++ b/spec/models/label_priority_spec.rb
@@ -18,5 +18,11 @@ RSpec.describe LabelPriority do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:project_id)
end
+
+ describe 'when importing' do
+ subject { create(:label_priority, importing: true) }
+
+ it { is_expected.not_to validate_presence_of(:label) }
+ end
end
end
diff --git a/spec/models/sentry_issue_spec.rb b/spec/models/sentry_issue_spec.rb
index 33654bf5e1a..c24350d7067 100644
--- a/spec/models/sentry_issue_spec.rb
+++ b/spec/models/sentry_issue_spec.rb
@@ -27,6 +27,12 @@ RSpec.describe SentryIssue do
expect(duplicate_sentry_issue).to be_invalid
expect(duplicate_sentry_issue.errors.full_messages.first).to include('is already associated')
end
+
+ describe 'when importing' do
+ subject { create(:sentry_issue, importing: true) }
+
+ it { is_expected.not_to validate_presence_of(:issue) }
+ end
end
describe 'callbacks' do
diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb
index e88fc13ecee..9a7624c253a 100644
--- a/spec/models/suggestion_spec.rb
+++ b/spec/models/suggestion_spec.rb
@@ -12,6 +12,12 @@ RSpec.describe Suggestion do
describe 'validations' do
it { is_expected.to validate_presence_of(:note) }
+ context 'when importing' do
+ subject { create(:suggestion, importing: true) }
+
+ it { is_expected.not_to validate_presence_of(:note) }
+ end
+
context 'when suggestion is applied' do
before do
allow(subject).to receive(:applied?).and_return(true)
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index ae1697fb7e6..e9019b55635 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -40,6 +40,14 @@ RSpec.describe Timelog do
expect(subject).to be_valid
end
+
+ describe 'when importing' do
+ it 'is valid if issue_id and merge_request_id are missing' do
+ subject.attributes = { issue: nil, merge_request: nil, importing: true }
+
+ expect(subject).to be_valid
+ end
+ end
end
describe 'scopes' do
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
index 00a0f92e848..2b45533035d 100644
--- a/spec/models/zoom_meeting_spec.rb
+++ b/spec/models/zoom_meeting_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe ZoomMeeting do
end
describe 'Associations' do
- it { is_expected.to belong_to(:project).required }
- it { is_expected.to belong_to(:issue).required }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:issue) }
end
describe 'scopes' do
@@ -40,6 +40,16 @@ RSpec.describe ZoomMeeting do
end
describe 'Validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:issue) }
+
+ describe 'when importing' do
+ subject { build(:zoom_meeting, importing: true) }
+
+ it { is_expected.not_to validate_presence_of(:project) }
+ it { is_expected.not_to validate_presence_of(:issue) }
+ end
+
describe 'url' do
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_length_of(:url).is_at_most(255) }
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index acc49768545..0e163ec2154 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -117,6 +117,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
)
end
+ it 'logs the event' do
+ expect(Feature.logger).to receive(:info).once
+
+ post api("/features/#{feature_name}", admin), params: { value: 'true' }
+ end
+
it 'creates an enabled feature for the given Flipper group when passed feature_group=perf_team' do
post api("/features/#{feature_name}", admin), params: { value: 'true', feature_group: 'perf_team' }
@@ -444,6 +450,12 @@ RSpec.describe API::Features, stub_feature_flags: false do
expect(response).to have_gitlab_http_status(:no_content)
end
+
+ it 'logs the event' do
+ expect(Feature.logger).to receive(:info).once
+
+ delete api("/features/#{feature_name}", admin)
+ end
end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 83b32d9b3fd..c50fe2c7e1d 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -883,7 +883,9 @@ RSpec.describe API::Projects do
only_allow_merge_if_all_discussions_are_resolved: false,
ci_config_path: 'a/custom/path',
merge_method: 'ff'
- })
+ }).tap do |attrs|
+ attrs[:operations_access_level] = 'disabled'
+ end
post api('/projects', user), params: project
@@ -900,6 +902,7 @@ RSpec.describe API::Projects do
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.operations_access_level).to eq(ProjectFeature::DISABLED)
end
it 'creates a project using a template' do
@@ -1579,6 +1582,7 @@ RSpec.describe API::Projects do
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+ expect(json_response['operations_access_level']).to be_present
end
end
@@ -1621,6 +1625,7 @@ RSpec.describe API::Projects do
expect(json_response['forking_access_level']).to be_present
expect(json_response['wiki_access_level']).to be_present
expect(json_response['builds_access_level']).to be_present
+ expect(json_response['operations_access_level']).to be_present
expect(json_response).to have_key('emails_disabled')
expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
expect(json_response['remove_source_branch_after_merge']).to be_truthy
diff --git a/spec/controllers/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index 39d92846863..805c1f1d82b 100644
--- a/spec/controllers/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -12,6 +12,6 @@ RSpec.describe IdeController do
it 'increases the views counter' do
expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
- get :index
+ get ide_url
end
end
diff --git a/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
new file mode 100644
index 00000000000..8c3703a488a
--- /dev/null
+++ b/spec/rubocop/cop/rspec/htt_party_basic_auth_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../../rubocop/cop/rspec/httparty_basic_auth'
+
+RSpec.describe RuboCop::Cop::RSpec::HTTPartyBasicAuth, type: :rubocop do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when passing `basic_auth: { user: ... }`' do
+ it 'registers an offence' do
+ expect_offense(<<~SOURCE, 'spec/foo.rb')
+ HTTParty.put(
+ url,
+ basic_auth: { user: user, password: token },
+ ^^^^ #{described_class::MESSAGE}
+ body: body
+ )
+ SOURCE
+ end
+
+ it 'can autocorrect the source' do
+ bad = 'HTTParty.put(url, basic_auth: { user: user, password: token })'
+ good = 'HTTParty.put(url, basic_auth: { username: user, password: token })'
+ expect(autocorrect_source(bad)).to eq(good)
+ end
+ end
+
+ context 'when passing `basic_auth: { username: ... }`' do
+ it 'does not register an offence' do
+ expect_no_offenses(<<~SOURCE, 'spec/frontend/fixtures/foo.rb')
+ HTTParty.put(
+ url,
+ basic_auth: { username: user, password: token },
+ body: body
+ )
+ SOURCE
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 93867ef2990..c19c26f9a0b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -142,7 +142,9 @@ RSpec.configure do |config|
config.include TestEnv
config.include FileReadHelpers
config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :view
config.include Devise::Test::IntegrationHelpers, type: :feature
+ config.include Devise::Test::IntegrationHelpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
config.include WaitHelpers, type: :feature
@@ -150,7 +152,6 @@ RSpec.configure do |config|
config.include EmailHelpers, :mailer, type: :mailer
config.include Warden::Test::Helpers, type: :request
config.include Gitlab::Routing, type: :routing
- config.include Devise::Test::ControllerHelpers, type: :view
config.include ApiHelpers, :api
config.include CookieHelper, :js
config.include InputHelper, :js
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index ed74c3f179f..549dc1cff1d 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -14,6 +14,15 @@ RSpec.shared_context 'project navbar structure' do
}
end
+ let(:security_and_compliance_nav_item) do
+ {
+ nav_item: _('Security & Compliance'),
+ nav_sub_items: [
+ _('Audit Events')
+ ]
+ }
+ end
+
let(:structure) do
[
{
@@ -62,6 +71,7 @@ RSpec.shared_context 'project navbar structure' do
_('Schedules')
]
},
+ (security_and_compliance_nav_item if Gitlab.ee?),
{
nav_item: _('Operations'),
nav_sub_items: [
@@ -101,8 +111,7 @@ RSpec.shared_context 'project navbar structure' do
_('Access Tokens'),
_('Repository'),
_('CI / CD'),
- _('Operations'),
- (_('Audit Events') if Gitlab.ee?)
+ _('Operations')
].compact
}
].compact
@@ -128,8 +137,7 @@ RSpec.shared_context 'group navbar structure' do
_('Projects'),
_('Repository'),
_('CI / CD'),
- _('Webhooks'),
- _('Audit Events')
+ _('Webhooks')
]
}
end
@@ -143,6 +151,15 @@ RSpec.shared_context 'group navbar structure' do
}
end
+ let(:security_and_compliance_nav_item) do
+ {
+ nav_item: _('Security & Compliance'),
+ nav_sub_items: [
+ _('Audit Events')
+ ]
+ }
+ end
+
let(:push_rules_nav_item) do
{
nav_item: _('Push Rules'),
@@ -172,6 +189,7 @@ RSpec.shared_context 'group navbar structure' do
nav_item: _('Merge Requests'),
nav_sub_items: []
},
+ (security_and_compliance_nav_item if Gitlab.ee?),
(push_rules_nav_item if Gitlab.ee?),
{
nav_item: _('Kubernetes'),
diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
index 9c0b398a5c1..2b93d174653 100644
--- a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
@@ -40,6 +40,30 @@ RSpec.shared_examples 'boards create mutation' do
end
end
+ context 'when hide_backlog_list parameter is true' do
+ before do
+ params[:hide_backlog_list] = true
+ end
+
+ it 'returns the board with correct hide_backlog_list field' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['board']['hideBacklogList']).to eq(true)
+ end
+ end
+
+ context 'when hide_closed_list parameter is true' do
+ before do
+ params[:hide_closed_list] = true
+ end
+
+ it 'returns the board with correct hide_closed_list field' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['board']['hideClosedList']).to eq(true)
+ end
+ end
+
context 'when the Boards::CreateService returns an error response' do
before do
allow_next_instance_of(Boards::CreateService) do |service|
diff --git a/yarn.lock b/yarn.lock
index ddf8ad4a57e..58e3a393196 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4186,10 +4186,10 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
-dompurify@^2.2.3:
- version "2.2.3"
- resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.3.tgz#ec653ba521b39f397c2ca045769438d593ea8a9f"
- integrity sha512-8Hv7Q0FuwD9rWoB6qI2eZsfKbGXfoUVuGHHrE15vgk4ReOKwOkSgbqb2OMFtc0d5besOEkoLkcyuV10zQ2X5gw==
+dompurify@^2.2.3, dompurify@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.4.tgz#a98cd182b729bdd8715c3eb7a8bf8eafb2ff7410"
+ integrity sha512-jE21SelIgWrGKoXGfGPA524Zt1IJFBnktwfFMHDlEYRx5FZOdc+4eEH9mkA6PuhExrq3HVpJnY8hMYUzAMl0OA==
domutils@^1.5.1:
version "1.6.2"