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--.gitlab-ci.yml52
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/alert_management/constants.js4
-rw-r--r--app/assets/javascripts/alerts_settings/services/index.js3
-rw-r--r--app/assets/javascripts/blob/sketch_viewer.js3
-rw-r--r--app/assets/javascripts/blob/template_selector.js3
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js5
-rw-r--r--app/assets/javascripts/build_artifacts.js3
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/utils.js5
-rw-r--r--app/assets/javascripts/constants.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/playable.js4
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js5
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js8
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js9
-rw-r--r--app/assets/javascripts/diffs/store/actions.js14
-rw-r--r--app/assets/javascripts/error_tracking/utils.js12
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js16
-rw-r--r--app/assets/javascripts/filtered_search/droplab/utils.js4
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js1
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js1
-rw-r--r--app/assets/javascripts/incidents/constants.js9
-rw-r--r--app/assets/javascripts/labels/create_label_dropdown.js3
-rw-r--r--app/assets/javascripts/locale/ensure_single_line.cjs2
-rw-r--r--app/assets/javascripts/monitoring/utils.js9
-rw-r--r--app/assets/javascripts/new_commit_form.js3
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue12
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue11
-rw-r--r--app/assets/javascripts/notes/utils.js5
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js6
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/syntax_highlight.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js8
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/constants.js9
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue3
-rw-r--r--app/assets/javascripts/zen_mode.js3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/finders/concerns/updated_at_filter.rb8
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb4
-rw-r--r--app/helpers/ide_helper.rb1
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/work_items/widgets/base.rb6
-rw-r--r--app/services/bulk_imports/create_service.rb43
-rw-r--r--app/services/concerns/work_items/widgetable_service.rb15
-rw-r--r--app/services/issuable/callbacks/base.rb27
-rw-r--r--app/services/issuable/callbacks/milestone.rb80
-rw-r--r--app/services/issuable_base_service.rb66
-rw-r--r--app/services/issues/after_create_service.rb1
-rw-r--r--app/services/issues/base_service.rb12
-rw-r--r--app/services/issues/build_service.rb5
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb5
-rw-r--r--app/services/issues/reopen_service.rb2
-rw-r--r--app/services/issues/update_service.rb30
-rw-r--r--app/services/merge_requests/after_create_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb6
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb20
-rw-r--r--app/services/packages/npm/create_package_service.rb24
-rw-r--r--app/services/tasks_to_be_done/base_service.rb6
-rw-r--r--app/services/work_items/widgets/milestone_service/create_service.rb13
-rw-r--r--app/services/work_items/widgets/milestone_service/update_service.rb13
-rw-r--r--config/feature_flags/development/deprecate_vulnerabilities_feedback.yml8
-rw-r--r--config/feature_flags/development/npm_obtain_lease_to_create_package.yml8
-rw-r--r--doc/administration/geo/replication/img/adding_a_secondary_v13_3.pngbin20195 -> 0 bytes
-rw-r--r--doc/api/graphql/reference/index.md20
-rw-r--r--doc/api/merge_requests.md3
-rw-r--r--doc/api/projects.md8
-rw-r--r--doc/development/documentation/topic_types/glossary.md70
-rw-r--r--doc/development/documentation/topic_types/index.md7
-rw-r--r--doc/development/stage_group_observability/dashboards/img/error_budget_detail_sli_detail.pngbin97895 -> 0 bytes
-rw-r--r--doc/development/work_items_widgets.md24
-rw-r--r--doc/user/group/saml_sso/index.md32
-rw-r--r--doc/user/img/observability_copy_shortened_link.pngbin15090 -> 0 bytes
-rw-r--r--doc/user/profile/account/two_factor_authentication.md2
-rw-r--r--doc/user/project/merge_requests/reviews/img/custom_commit_v13_9.pngbin41069 -> 0 bytes
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb3
-rw-r--r--lib/api/projects.rb23
-rw-r--r--lib/bulk_imports/clients/http.rb2
-rw-r--r--lib/bulk_imports/error.rb9
-rw-r--r--spec/features/issues/form_spec.rb2
-rw-r--r--spec/finders/projects_finder_spec.rb48
-rw-r--r--spec/frontend/diffs/store/actions_spec.js64
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js3
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js11
-rw-r--r--spec/graphql/types/project_type_spec.rb1
-rw-r--r--spec/helpers/ide_helper_spec.rb3
-rw-r--r--spec/lib/bulk_imports/clients/graphql_spec.rb6
-rw-r--r--spec/requests/api/bulk_imports_spec.rb36
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb22
-rw-r--r--spec/requests/api/merge_requests_spec.rb29
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb21
-rw-r--r--spec/requests/api/projects_spec.rb81
-rw-r--r--spec/services/bulk_imports/create_service_spec.rb144
-rw-r--r--spec/services/issuable/callbacks/milestone_spec.rb85
-rw-r--r--spec/services/issues/after_create_service_spec.rb7
-rw-r--r--spec/services/issues/build_service_spec.rb4
-rw-r--r--spec/services/issues/create_service_spec.rb9
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb16
-rw-r--r--spec/services/merge_requests/create_service_spec.rb14
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb66
-rw-r--r--spec/services/tasks_to_be_done/base_service_spec.rb4
-rw-r--r--spec/services/work_items/widgets/milestone_service/create_service_spec.rb28
-rw-r--r--spec/services/work_items/widgets/milestone_service/update_service_spec.rb58
-rw-r--r--spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb42
-rw-r--r--spec/tooling/lib/tooling/find_tests_spec.rb16
-rw-r--r--spec/tooling/lib/tooling/helpers/file_handler_spec.rb108
-rw-r--r--spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb10
-rw-r--r--tooling/lib/tooling/find_tests.rb12
-rw-r--r--tooling/lib/tooling/helpers/file_handler.rb23
-rw-r--r--tooling/lib/tooling/mappings/base.rb3
-rw-r--r--tooling/lib/tooling/mappings/partial_to_views_mappings.rb4
116 files changed, 1353 insertions, 508 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3b2f56cb5c0..e39fa9c4d76 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -26,12 +26,12 @@ default:
# Default job timeout set to 90m https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10520
timeout: 90m
-.ruby3-variables: &ruby3-variables
+.default-ruby-variables: &default-ruby-variables
RUBY_VERSION: "3.0"
OMNIBUS_GITLAB_RUBY3_BUILD: "true"
OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY3"
-.ruby2-variables: &ruby2-variables
+.backcompat-ruby-variables: &backcompat-ruby-variables
RUBY_VERSION: "2.7"
OMNIBUS_GITLAB_RUBY2_BUILD: "true"
OMNIBUS_GITLAB_CACHE_EDITION: "GITLAB_RUBY2"
@@ -49,8 +49,8 @@ workflow:
# If `$FORCE_GITLAB_CI` is set, create a pipeline.
- if: '$FORCE_GITLAB_CI'
variables:
- <<: *ruby3-variables
- PIPELINE_NAME: 'Ruby 3 forced pipeline'
+ <<: *default-ruby-variables
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION forced pipeline'
# As part of the process of creating RCs automatically, we update stable
# branches with the changes of the most recent production deployment. The
# merge requests used for this merge a branch release-tools/X into a stable
@@ -61,73 +61,73 @@ workflow:
# For merge requests running exclusively in Ruby 2.7
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby2/'
variables:
- <<: *ruby2-variables
- PIPELINE_NAME: 'Ruby 2 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
+ <<: *backcompat-ruby-variables
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
NO_SOURCEMAPS: 'true'
- if: '$CI_MERGE_REQUEST_LABELS =~ /Community contribution/'
variables:
- <<: *ruby3-variables
+ <<: *default-ruby-variables
GITLAB_DEPENDENCY_PROXY_ADDRESS: ""
- PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline (community contribution)'
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline (community contribution)'
NO_SOURCEMAPS: 'true'
# For (detached) merge request pipelines.
- if: '$CI_MERGE_REQUEST_IID'
variables:
- <<: *ruby3-variables
- PIPELINE_NAME: 'Ruby 3 $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
+ <<: *default-ruby-variables
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_MERGE_REQUEST_EVENT_TYPE MR pipeline'
NO_SOURCEMAPS: 'true'
# For the scheduled pipelines, we set specific variables.
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"'
variables:
- <<: *ruby3-variables
+ <<: *default-ruby-variables
<<: *default-branch-pipeline-failure-variables
CRYSTALBALL: "true"
- PIPELINE_NAME: 'Scheduled Ruby 3 $CI_COMMIT_BRANCH branch pipeline'
+ PIPELINE_NAME: 'Scheduled Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline'
# Run pipelines for ruby2 branch
- if: '$CI_COMMIT_BRANCH == "ruby2" && $CI_PIPELINE_SOURCE == "schedule"'
variables:
- <<: *ruby2-variables
+ <<: *backcompat-ruby-variables
NOTIFY_PIPELINE_FAILURE_CHANNEL: "f_ruby3"
- PIPELINE_NAME: 'Scheduled Ruby 2 $CI_COMMIT_BRANCH branch pipeline'
+ PIPELINE_NAME: 'Scheduled Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline'
# This work around https://gitlab.com/gitlab-org/gitlab/-/issues/332411 whichs prevents usage of dependency proxy
# when pipeline is triggered by a project access token.
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $GITLAB_USER_LOGIN =~ /project_\d+_bot\d*/'
variables:
- <<: *ruby3-variables
+ <<: *default-ruby-variables
<<: *default-branch-pipeline-failure-variables
GITLAB_DEPENDENCY_PROXY_ADDRESS: ""
- PIPELINE_NAME: 'Ruby 3 $CI_COMMIT_BRANCH branch pipeline (triggered by a project token)'
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline (triggered by a project token)'
# For `$CI_DEFAULT_BRANCH` branch, create a pipeline (this includes on schedules, pushes, merges, etc.).
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
variables:
- <<: *ruby3-variables
+ <<: *default-ruby-variables
<<: *default-branch-pipeline-failure-variables
- PIPELINE_NAME: 'Ruby 3 $CI_COMMIT_BRANCH branch pipeline'
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline'
# For tags, create a pipeline.
- if: '$CI_COMMIT_TAG'
variables:
- <<: *ruby3-variables
- PIPELINE_NAME: 'Ruby 3 $CI_COMMIT_TAG tag pipeline'
+ <<: *default-ruby-variables
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_TAG tag pipeline'
# If `$GITLAB_INTERNAL` isn't set, don't create a pipeline.
- if: '$GITLAB_INTERNAL == null'
when: never
# For stable, auto-deploy, and security branches, create a pipeline.
- if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable(-ee)?$/'
variables:
- <<: *ruby3-variables
+ <<: *default-ruby-variables
NOTIFY_PIPELINE_FAILURE_CHANNEL: "releases"
- PIPELINE_NAME: 'Ruby 3 $CI_COMMIT_BRANCH branch pipeline'
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline'
CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true"
BROKEN_BRANCH_INCIDENTS_PROJECT: "gitlab-org/release/tasks"
BROKEN_BRANCH_INCIDENTS_PROJECT_TOKEN: "${BROKEN_STABLE_INCIDENTS_PROJECT_TOKEN}"
- if: '$CI_COMMIT_BRANCH =~ /^\d+-\d+-auto-deploy-\d+$/'
variables:
- <<: *ruby3-variables
- PIPELINE_NAME: 'Ruby 3 $CI_COMMIT_BRANCH branch pipeline'
+ <<: *default-ruby-variables
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline'
- if: '$CI_COMMIT_BRANCH =~ /^security\//'
variables:
- <<: *ruby3-variables
- PIPELINE_NAME: 'Ruby 3 $CI_COMMIT_BRANCH branch pipeline'
+ <<: *default-ruby-variables
+ PIPELINE_NAME: 'Ruby $RUBY_VERSION $CI_COMMIT_BRANCH branch pipeline'
variables:
PG_VERSION: "12"
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 9638906b90e..342561afd0c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-4726cac92d47e773b875c6e64ebd4a3a73e0e69d
+4a6eb3bfb6ecd8324a39089cb3b59e282936331c
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index c98d3865621..4ab772e4523 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -37,12 +37,10 @@ export const ALERTS_STATUS_TABS = [
},
];
-/* eslint-disable @gitlab/require-i18n-strings */
-
/**
* Tracks snowplow event when user views alerts list
*/
export const trackAlertListViewsOptions = {
- category: 'Alert Management',
+ category: 'Alert Management', // eslint-disable-line @gitlab/require-i18n-strings
action: 'view_alerts_list',
};
diff --git a/app/assets/javascripts/alerts_settings/services/index.js b/app/assets/javascripts/alerts_settings/services/index.js
index e45ea772ddd..4df5ed425a5 100644
--- a/app/assets/javascripts/alerts_settings/services/index.js
+++ b/app/assets/javascripts/alerts_settings/services/index.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import axios from '~/lib/utils/axios_utils';
export default {
@@ -9,7 +8,7 @@ export default {
return axios.post(endpoint, data, {
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
+ Authorization: `Bearer ${token}`, // eslint-disable-line @gitlab/require-i18n-strings
},
});
},
diff --git a/app/assets/javascripts/blob/sketch_viewer.js b/app/assets/javascripts/blob/sketch_viewer.js
index 2c1c6339fdb..834aa3e5354 100644
--- a/app/assets/javascripts/blob/sketch_viewer.js
+++ b/app/assets/javascripts/blob/sketch_viewer.js
@@ -1,8 +1,7 @@
-/* eslint-disable no-new */
import SketchLoader from './sketch';
export default () => {
const el = document.getElementById('js-sketch-viewer');
- new SketchLoader(el);
+ new SketchLoader(el); // eslint-disable-line no-new
};
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 7eb699eacbe..59b7f82c10e 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -1,5 +1,3 @@
-/* eslint-disable class-methods-use-this */
-
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
@@ -70,6 +68,7 @@ export default class TemplateSelector {
return this.requestFile(item);
}
+ // eslint-disable-next-line class-methods-use-this
requestFile() {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 01d35a0980f..7e667409556 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new */
-
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { createAlert } from '~/alert';
@@ -54,6 +52,7 @@ export default () => {
import('./edit_blob')
.then(({ default: EditBlob } = {}) => {
+ // eslint-disable-next-line no-new
new EditBlob({
assetsPath: `${urlRoot}${assetsPath}`,
filePath,
@@ -80,7 +79,7 @@ export default () => {
window.onbeforeunload = null;
});
- new NewCommitForm(editBlobForm);
+ new NewCommitForm(editBlobForm); // eslint-disable-line no-new
// returning here blocks page navigation
window.onbeforeunload = () => '';
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index e895df01f2c..4d1c4be73a3 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,5 +1,3 @@
-/* eslint-disable func-names */
-
import $ from 'jquery';
import { hide, initTooltips, show } from '~/tooltips';
import { parseBoolean } from './lib/utils/common_utils';
@@ -24,6 +22,7 @@ export default class BuildArtifacts {
// eslint-disable-next-line class-methods-use-this
setupEntryClick() {
+ // eslint-disable-next-line func-names
return $('.tree-holder').on('click', 'tr[data-link]', function () {
visitUrl(this.dataset.link, parseBoolean(this.dataset.externalLink));
});
diff --git a/app/assets/javascripts/ci/runner/components/registration/utils.js b/app/assets/javascripts/ci/runner/components/registration/utils.js
index 94d75bc4562..d1ba646ad0c 100644
--- a/app/assets/javascripts/ci/runner/components/registration/utils.js
+++ b/app/assets/javascripts/ci/runner/components/registration/utils.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import {
DEFAULT_PLATFORM,
LINUX_PLATFORM,
@@ -60,7 +59,7 @@ export const registerCommand = ({
registrationToken,
description,
}) => {
- const lines = [`${executable({ platform })} register`];
+ const lines = [`${executable({ platform })} register`]; // eslint-disable-line @gitlab/require-i18n-strings
if (url) {
lines.push(` --url ${url}`);
}
@@ -75,7 +74,7 @@ export const registerCommand = ({
};
export const runCommand = ({ platform }) => {
- return `${executable({ platform })} run`;
+ return `${executable({ platform })} run`; // eslint-disable-line @gitlab/require-i18n-strings
};
const importInstallScript = ({ platform = DEFAULT_PLATFORM }) => {
diff --git a/app/assets/javascripts/constants.js b/app/assets/javascripts/constants.js
index defc2cbe276..f43a2d5d8ff 100644
--- a/app/assets/javascripts/constants.js
+++ b/app/assets/javascripts/constants.js
@@ -1,6 +1,5 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
export const getModifierKey = (removeSuffix = false) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
const winKey = `Ctrl${removeSuffix ? '' : '+'}`;
return window.gl?.client?.isMac ? '⌘' : winKey;
};
diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js
index ed343d8acf8..01ffc217894 100644
--- a/app/assets/javascripts/content_editor/extensions/playable.js
+++ b/app/assets/javascripts/content_editor/extensions/playable.js
@@ -1,5 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import { Node } from '@tiptap/core';
const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType);
@@ -44,7 +42,7 @@ export default Node.create({
parseHTML() {
return [
{
- tag: `.${this.options.mediaType}-container`,
+ tag: `.${this.options.mediaType}-container`, // eslint-disable-line @gitlab/require-i18n-strings
},
];
},
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 540815f57c9..0a4353ab3dd 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -638,9 +638,8 @@ const generateStrikeTag = (wrapTagName = openTag) => {
switch (type) {
case '~~':
return type;
- /* eslint-disable @gitlab/require-i18n-strings */
- case '<del':
- case '<strike':
+ case '<del': // eslint-disable-line @gitlab/require-i18n-strings
+ case '<strike': // eslint-disable-line @gitlab/require-i18n-strings
case '<s':
return wrapTagName(type.substring(1));
default:
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
index 8ca4dc587a8..2cb9e9a56a3 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
@@ -1,5 +1,3 @@
-/* eslint-disable consistent-return */
-
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import $ from 'jquery';
import { debounce } from 'lodash';
@@ -59,6 +57,7 @@ export class GitLabDropdownFilter {
return BLUR_KEYCODES.indexOf(keyCode) !== -1;
}
+ // eslint-disable-next-line consistent-return
filter(searchText) {
let group;
let results;
@@ -114,9 +113,10 @@ export class GitLabDropdownFilter {
const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
if (!$el.is('.dropdown-header')) {
if (matches.length) {
- return $el.show().removeClass('option-hidden');
+ $el.show().removeClass('option-hidden');
+ } else {
+ $el.hide().addClass('option-hidden');
}
- return $el.hide().addClass('option-hidden');
}
});
} else {
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index 9ef0f336d43..1ae7b6a2110 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -1,8 +1,7 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import produce from 'immer';
import { differenceBy } from 'lodash';
import { createAlert } from '~/alert';
+import { TYPENAME_DISCUSSION, TYPENAME_TODO, TYPENAME_USER } from '~/graphql_shared/constants';
import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
@@ -60,7 +59,7 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
});
const newDiscussion = {
- __typename: 'Discussion',
+ __typename: TYPENAME_DISCUSSION,
id: createImageDiffNote.note.discussion.id,
replyId: createImageDiffNote.note.discussion.replyId,
resolvable: true,
@@ -86,7 +85,7 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
design.issue.participants.nodes = [
...design.issue.participants.nodes,
{
- __typename: 'User',
+ __typename: TYPENAME_USER,
...createImageDiffNote.note.author,
},
];
@@ -199,7 +198,7 @@ export const addPendingTodoToStore = (store, pendingTodo, query, queryVariables)
const data = produce(sourceData, (draftData) => {
const design = extractDesign(draftData);
const existingTodos = design.currentUserTodos?.nodes || [];
- const newTodoNodes = [...existingTodos, { ...pendingTodo, __typename: 'Todo' }];
+ const newTodoNodes = [...existingTodos, { ...pendingTodo, __typename: TYPENAME_TODO }];
if (!design.currentUserTodos) {
design.currentUserTodos = {
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 9c5f2043933..29b77431f7d 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -16,6 +16,7 @@ import { __, s__ } from '~/locale';
import notesEventHub from '~/notes/event_hub';
import { generateTreeList } from '~/diffs/utils/tree_worker_utils';
import { sortTree } from '~/ide/stores/utils';
+import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
@@ -555,13 +556,20 @@ export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
}
};
-export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
+export const saveDiffDiscussion = async ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({
commit: state.commit,
note,
...formData,
});
+ if (containsSensitiveToken(note)) {
+ const confirmed = await confirmSensitiveAction();
+ if (!confirmed) {
+ return null;
+ }
+ }
+
return dispatch('saveNote', postData, { root: true })
.then((result) => dispatch('updateDiscussion', result.discussion, { root: true }))
.then((discussion) => dispatch('assignDiscussionsToDiff', [discussion]))
@@ -822,13 +830,11 @@ export const setSuggestPopoverDismissed = ({ commit, state }) =>
});
export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) {
- /* eslint-disable @gitlab/require-i18n-strings */
if (!commitId) {
return Promise.reject(new Error('`commitId` is a required argument'));
} else if (!state.commit) {
- return Promise.reject(new Error('`state` must already contain a valid `commit`'));
+ return Promise.reject(new Error('`state` must already contain a valid `commit`')); // eslint-disable-line @gitlab/require-i18n-strings
}
- /* eslint-enable @gitlab/require-i18n-strings */
// this is less than ideal, see: https://gitlab.com/gitlab-org/gitlab/-/issues/215421
const commitRE = new RegExp(state.commit.id, 'g');
diff --git a/app/assets/javascripts/error_tracking/utils.js b/app/assets/javascripts/error_tracking/utils.js
index aeed5450022..afb91d3db51 100644
--- a/app/assets/javascripts/error_tracking/utils.js
+++ b/app/assets/javascripts/error_tracking/utils.js
@@ -1,13 +1,13 @@
-/* eslint-disable @gitlab/require-i18n-strings */
+const category = 'Error Tracking'; // eslint-disable-line @gitlab/require-i18n-strings
/**
* Tracks snowplow event when User clicks on error link to Sentry
* @param {String} externalUrl that will be send as a property for the event
*/
export const trackClickErrorLinkToSentryOptions = (url) => ({
- category: 'Error Tracking',
+ category,
action: 'click_error_link_to_sentry',
- label: 'Error Link',
+ label: 'Error Link', // eslint-disable-line @gitlab/require-i18n-strings
property: url,
});
@@ -15,7 +15,7 @@ export const trackClickErrorLinkToSentryOptions = (url) => ({
* Tracks snowplow event when user views error list
*/
export const trackErrorListViewsOptions = {
- category: 'Error Tracking',
+ category,
action: 'view_errors_list',
};
@@ -23,7 +23,7 @@ export const trackErrorListViewsOptions = {
* Tracks snowplow event when user views error details
*/
export const trackErrorDetailsViewsOptions = {
- category: 'Error Tracking',
+ category,
action: 'view_error_details',
};
@@ -31,6 +31,6 @@ export const trackErrorDetailsViewsOptions = {
* Tracks snowplow event when error status is updated
*/
export const trackErrorStatusUpdateOptions = (status) => ({
- category: 'Error Tracking',
+ category,
action: `update_${status}_status`,
});
diff --git a/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js b/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js
index 148d9a35b81..c2c46e4265a 100644
--- a/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/input_setter.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
const InputSetter = {
init(hook) {
this.hook = hook;
@@ -33,11 +31,15 @@ const InputSetter = {
setInput(config, selectedItem) {
const input = config.input || this.hook.trigger;
const newValue = selectedItem.getAttribute(config.valueAttribute);
- const inputAttribute = config.inputAttribute;
-
- if (input.hasAttribute(inputAttribute)) return input.setAttribute(inputAttribute, newValue);
- if (input.tagName === 'INPUT') return (input.value = newValue);
- return (input.textContent = newValue);
+ const { inputAttribute } = config;
+
+ if (input.hasAttribute(inputAttribute)) {
+ input.setAttribute(inputAttribute, newValue);
+ } else if (input.tagName === 'INPUT') {
+ input.value = newValue;
+ } else {
+ input.textContent = newValue;
+ }
},
destroy() {
diff --git a/app/assets/javascripts/filtered_search/droplab/utils.js b/app/assets/javascripts/filtered_search/droplab/utils.js
index d7f49bf19d8..3d3470a16d0 100644
--- a/app/assets/javascripts/filtered_search/droplab/utils.js
+++ b/app/assets/javascripts/filtered_search/droplab/utils.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
import { template as _template } from 'lodash';
import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants';
@@ -26,7 +24,7 @@ const utils = {
closest(thisTag, stopTag) {
while (thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML') {
- thisTag = thisTag.parentNode;
+ thisTag = thisTag.parentNode; // eslint-disable-line no-param-reassign
}
return thisTag;
},
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 77fca45c949..65aa38cfb99 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -22,6 +22,7 @@ export const TYPENAME_PACKAGES_PACKAGE = 'Packages::Package';
export const TYPENAME_PROJECT = 'Project';
export const TYPENAME_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPENAME_SITE_PROFILE = 'DastSiteProfile';
+export const TYPENAME_TODO = 'Todo';
export const TYPENAME_USER = 'User';
export const TYPENAME_VULNERABILITIES_SCANNER = 'Vulnerabilities::Scanner';
export const TYPENAME_VULNERABILITY = 'Vulnerability';
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 4d3cefcb107..51af73decad 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -67,6 +67,7 @@ export const initGitlabWebIDE = async (el) => {
links: {
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
userPreferences: el.dataset.userPreferencesPath,
+ signIn: el.dataset.signInPath,
},
editorFont: {
srcUrl: editorFontSrcUrl,
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index dde40ec2983..6f8d5cf5f89 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import { s__ } from '~/locale';
export const I18N = {
@@ -51,11 +50,13 @@ export const TH_INCIDENT_SLA_TEST_ID = { 'data-testid': 'incident-management-sla
export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
export const INCIDENT_DETAILS_PATH = 'incident';
+const category = 'Incident Management'; // eslint-disable-line @gitlab/require-i18n-strings
+
/**
* Tracks snowplow event when user clicks create new incident
*/
export const trackIncidentCreateNewOptions = {
- category: 'Incident Management',
+ category,
action: 'create_incident_button_clicks',
};
@@ -63,7 +64,7 @@ export const trackIncidentCreateNewOptions = {
* Tracks snowplow event when user views incidents list
*/
export const trackIncidentListViewsOptions = {
- category: 'Incident Management',
+ category,
action: 'view_incidents_list',
};
@@ -71,6 +72,6 @@ export const trackIncidentListViewsOptions = {
* Tracks snowplow event when user views incident details
*/
export const trackIncidentDetailsViewsOptions = {
- category: 'Incident Management',
+ category,
action: 'view_incident_details',
};
diff --git a/app/assets/javascripts/labels/create_label_dropdown.js b/app/assets/javascripts/labels/create_label_dropdown.js
index 60ab0c92256..fa0104fcf12 100644
--- a/app/assets/javascripts/labels/create_label_dropdown.js
+++ b/app/assets/javascripts/labels/create_label_dropdown.js
@@ -1,5 +1,3 @@
-/* eslint-disable func-names */
-
import $ from 'jquery';
import Api from '~/api';
import { humanize } from '~/lib/utils/text_utility';
@@ -49,6 +47,7 @@ export default class CreateLabelDropdown {
addBinding() {
const self = this;
+ // eslint-disable-next-line func-names
this.$colorSuggestions.on('click', function (e) {
const $this = $(this);
self.addColorValue(e, $this);
diff --git a/app/assets/javascripts/locale/ensure_single_line.cjs b/app/assets/javascripts/locale/ensure_single_line.cjs
index c2c63777001..f7790cadc48 100644
--- a/app/assets/javascripts/locale/ensure_single_line.cjs
+++ b/app/assets/javascripts/locale/ensure_single_line.cjs
@@ -1,5 +1,3 @@
-/* eslint-disable import/no-commonjs */
-
const SPLIT_REGEX = /\s*[\r\n]+\s*/;
/**
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 0d849e1a2d8..5f4d2703d21 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -97,7 +97,6 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
*/
const isClusterHealthBoard = () => (document.body.dataset.page || '').includes(':clusters:show');
-/* eslint-disable @gitlab/require-i18n-strings */
/**
* Tracks snowplow event when user generates link to metric chart
* @param {String} chart link that will be sent as a property for the event
@@ -107,13 +106,13 @@ export const generateLinkToChartOptions = (chartLink) => {
const isCLusterHealthBoard = isClusterHealthBoard();
const category = isCLusterHealthBoard
- ? 'Cluster Monitoring'
+ ? 'Cluster Monitoring' // eslint-disable-line @gitlab/require-i18n-strings
: 'Incident Management::Embedded metrics';
const action = isCLusterHealthBoard
? 'generate_link_to_cluster_metric_chart'
: 'generate_link_to_metrics_chart';
- return { category, action, label: 'Chart link', property: chartLink };
+ return { category, action, label: 'Chart link', property: chartLink }; // eslint-disable-line @gitlab/require-i18n-strings
};
/**
@@ -125,13 +124,13 @@ export const downloadCSVOptions = (title) => {
const isCLusterHealthBoard = isClusterHealthBoard();
const category = isCLusterHealthBoard
- ? 'Cluster Monitoring'
+ ? 'Cluster Monitoring' // eslint-disable-line @gitlab/require-i18n-strings
: 'Incident Management::Embedded metrics';
const action = isCLusterHealthBoard
? 'download_csv_of_cluster_metric_chart'
: 'download_csv_of_metrics_dashboard_chart';
- return { category, action, label: 'Chart title', property: title };
+ return { category, action, label: 'Chart title', property: title }; // eslint-disable-line @gitlab/require-i18n-strings
};
/* eslint-enable @gitlab/require-i18n-strings */
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 037be8467cb..c76ffce9168 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-return-assign */
export default class NewCommitForm {
constructor(form) {
this.form = form;
@@ -21,6 +20,6 @@ export default class NewCommitForm {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
}
- return (this.wasDifferent = different);
+ this.wasDifferent = different;
}
}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 60ae573bae7..3375e366ecf 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -11,6 +11,7 @@ import { s__, __, sprintf } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
@@ -207,12 +208,21 @@ export default {
this.isReplying = false;
clearDraft(this.autosaveKey);
}),
- saveReply(noteText, form, callback) {
+ async saveReply(noteText, form, callback) {
if (!noteText) {
this.cancelReplyForm();
callback();
return;
}
+
+ if (containsSensitiveToken(noteText)) {
+ const confirmed = await confirmSensitiveAction();
+ if (!confirmed) {
+ callback();
+ return;
+ }
+ }
+
const postData = {
in_reply_to_discussion_id: this.discussion.reply_id,
target_type: this.getNoteableData.targetType,
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index d9340556012..ae2f94a5a80 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -13,6 +13,7 @@ import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { __, s__, sprintf } from '~/locale';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
@@ -296,7 +297,7 @@ export default {
renderGFM(this.$refs.noteBody.$el);
this.$emit('updateSuccess');
},
- formUpdateHandler({ noteText, callback, resolveDiscussion }) {
+ async formUpdateHandler({ noteText, callback, resolveDiscussion }) {
const position = {
...this.note.position,
};
@@ -319,6 +320,14 @@ export default {
if (this.isDraft) return;
+ if (containsSensitiveToken(noteText)) {
+ const confirmed = await confirmSensitiveAction();
+ if (!confirmed) {
+ callback();
+ return;
+ }
+ }
+
const data = {
endpoint: this.note.path,
note: {
diff --git a/app/assets/javascripts/notes/utils.js b/app/assets/javascripts/notes/utils.js
index 14e97fcef46..9a1323cdaf2 100644
--- a/app/assets/javascripts/notes/utils.js
+++ b/app/assets/javascripts/notes/utils.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import { marked } from 'marked';
import { sanitize } from '~/lib/dompurify';
import { markdownConfig } from '~/lib/utils/text_utility';
@@ -8,9 +7,9 @@ import { markdownConfig } from '~/lib/utils/text_utility';
* @param {Boolean} enabled that will be send as a property for the event
*/
export const trackToggleTimelineView = (enabled) => ({
- category: 'Incident Management',
+ category: 'Incident Management', // eslint-disable-line @gitlab/require-i18n-strings
action: 'toggle_incident_comments_into_timeline_view',
- label: 'Status',
+ label: 'Status', // eslint-disable-line @gitlab/require-i18n-strings
property: enabled,
});
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 52124865bcc..dba65c7e791 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new */
-
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
@@ -7,11 +5,11 @@ import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list';
export default function initGroupDetails() {
- new ShortcutsNavigation();
+ new ShortcutsNavigation(); // eslint-disable-line no-new
initNotificationsDropdown();
- new ProjectsList();
+ new ProjectsList(); // eslint-disable-line no-new
initInviteMembersBanner();
initInviteMembersModal();
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 8f76d7535f1..83cd64c17ed 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -255,7 +255,7 @@ export default {
this.canRefetchHeaderPipeline = true;
this.$apollo.queries.headerPipeline.refetch();
},
- /* eslint-disable @gitlab/require-i18n-strings */
+ // eslint-disable-next-line @gitlab/require-i18n-strings
reportFailure({ type, err = 'No error string passed.', skipSentry = false }) {
this.showAlert = true;
this.alertType = type;
@@ -263,7 +263,6 @@ export default {
reportToSentry(this.$options.name, `type: ${type}, info: ${err}`);
}
},
- /* eslint-enable @gitlab/require-i18n-strings */
updateShowLinksState(val) {
this.showLinks = val;
},
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
index 3da792cb9df..54985a24593 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/pipelines/components/graph/utils.js
@@ -35,7 +35,6 @@ const calculatePipelineLayersInfo = (pipeline, componentName, metricsPath) => {
return layers;
};
-/* eslint-disable @gitlab/require-i18n-strings */
const getQueryHeaders = (etagResource) => {
return {
fetchOptions: {
@@ -52,6 +51,7 @@ const getQueryHeaders = (etagResource) => {
const serializeGqlErr = (gqlError) => {
const { locations = [], message = '', path = [] } = gqlError;
+ // eslint-disable-next-line @gitlab/require-i18n-strings
return `
${message}.
Locations: ${locations
@@ -74,14 +74,12 @@ const serializeLoadErrors = (errors) => {
}
if (!isEmpty(networkError)) {
- return `Network error: ${networkError.message}`;
+ return `Network error: ${networkError.message}`; // eslint-disable-line @gitlab/require-i18n-strings
}
return message;
};
-/* eslint-enable @gitlab/require-i18n-strings */
-
const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => {
const stopStartQuery = (query) => {
if (!Visibility.hidden()) {
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index b613e356a7a..bab167bb7e4 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,5 +1,3 @@
-/* eslint-disable consistent-return */
-
import $ from 'jquery';
import { createAlert } from '~/alert';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
@@ -66,7 +64,7 @@ export default class SingleFileDiff {
} else {
this.$chevronDownIcon.removeClass('gl-display-none');
this.$chevronRightIcon.addClass('gl-display-none');
- return this.getContentHTML(cb);
+ return this.getContentHTML(cb); // eslint-disable-line consistent-return
}
}
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 065e1080897..d79252f6bb7 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,5 +1,3 @@
-/* eslint-disable consistent-return */
-
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
@@ -14,6 +12,7 @@ export default function syntaxHighlight($els = null) {
if (!$els || $els.length === 0) return;
const els = $els.get ? $els.get() : $els;
+ // eslint-disable-next-line consistent-return
const handler = (el) => {
if (el.classList === undefined) {
return el;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
index 20284c4a3d8..26527361b2e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable @gitlab/require-i18n-strings */
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { escapeShellString } from '~/lib/utils/text_utility';
@@ -87,11 +86,11 @@ export default {
const escapedOriginBranch = escapeShellString(`origin/${this.sourceBranch}`);
return this.isFork
- ? `git fetch "${this.sourceProjectDefaultUrl}" ${this.escapedSourceBranch}\ngit checkout -b ${this.escapedForkBranch} FETCH_HEAD`
- : `git fetch origin\ngit checkout -b ${this.escapedSourceBranch} ${escapedOriginBranch}`;
+ ? `git fetch "${this.sourceProjectDefaultUrl}" ${this.escapedSourceBranch}\ngit checkout -b ${this.escapedForkBranch} FETCH_HEAD` // eslint-disable-line @gitlab/require-i18n-strings
+ : `git fetch origin\ngit checkout -b ${this.escapedSourceBranch} ${escapedOriginBranch}`; // eslint-disable-line @gitlab/require-i18n-strings
},
mergeInfo2() {
- return `git push origin ${this.escapedSourceBranch}`;
+ return `git push origin ${this.escapedSourceBranch}`; // eslint-disable-line @gitlab/require-i18n-strings
},
escapedForkBranch() {
return escapeShellString(`${this.sourceProjectPath}-${this.sourceBranch}`);
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 183f450854a..a2f088a7a58 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,7 +1,3 @@
-// This is a false violation of @gitlab/no-runtime-template-compiler, since it
-// creates a new Vue instance by spreading a _valid_ Vue component definition
-// into the Vue constructor.
-/* eslint-disable @gitlab/no-runtime-template-compiler */
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
@@ -33,6 +29,10 @@ export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url;
+ // This is a false violation of @gitlab/no-runtime-template-compiler, since it
+ // creates a new Vue instance by spreading a _valid_ Vue component definition
+ // into the Vue constructor.
+ // eslint-disable-next-line @gitlab/no-runtime-template-compiler
const vm = new Vue({
el: '#js-vue-mr-widget',
provide: {
diff --git a/app/assets/javascripts/vue_shared/alert_details/constants.js b/app/assets/javascripts/vue_shared/alert_details/constants.js
index d106f545c61..4ee8d19770d 100644
--- a/app/assets/javascripts/vue_shared/alert_details/constants.js
+++ b/app/assets/javascripts/vue_shared/alert_details/constants.js
@@ -9,7 +9,8 @@ export const SEVERITY_LEVELS = {
UNKNOWN: s__('severity|Unknown'),
};
-/* eslint-disable @gitlab/require-i18n-strings */
+const category = 'Alert Management'; // eslint-disable-line @gitlab/require-i18n-strings
+
export const PAGE_CONFIG = {
OPERATIONS: {
TITLE: 'OPERATIONS',
@@ -20,14 +21,14 @@ export const PAGE_CONFIG = {
},
// Tracks snowplow event when user views alert details
TRACK_ALERTS_DETAILS_VIEWS_OPTIONS: {
- category: 'Alert Management',
+ category,
action: 'view_alert_details',
},
// Tracks snowplow event when alert status is updated
TRACK_ALERT_STATUS_UPDATE_OPTIONS: {
- category: 'Alert Management',
+ category,
action: 'update_alert_status',
- label: 'Status',
+ label: 'Status', // eslint-disable-line @gitlab/require-i18n-strings
},
},
};
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index 017f9a69444..7fa99958b9d 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -126,8 +126,7 @@ export default {
this.isExpanded = !this.isExpanded;
},
threadKey(note) {
- /* eslint-disable @gitlab/require-i18n-strings */
- return `${note.id}-thread`;
+ return `${note.id}-thread`; // eslint-disable-line @gitlab/require-i18n-strings
},
onReplied() {
this.isExpanded = true;
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 1aa3baca165..ccb9d05bc90 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,5 +1,3 @@
-/* eslint-disable consistent-return */
-
// Zen Mode (full screen) textarea
//
/*= provides zen_mode:enter */
@@ -55,6 +53,7 @@ export default class ZenMode {
$(document).on('zen_mode:leave', () => {
this.exit();
});
+ // eslint-disable-next-line consistent-return
$(document).on('keydown', (e) => {
// Esc
if (e.keyCode === 27) {
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 0851d2ef3e2..35cf60a2c24 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -37,6 +37,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
push_frontend_feature_flag(:issue_assignees_widget, @project)
push_frontend_feature_flag(:refactor_security_extension, @project)
+ push_frontend_feature_flag(:deprecate_vulnerabilities_feedback, @project)
push_frontend_feature_flag(:refactor_code_quality_inline_findings, project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_frontend_feature_flag(:mr_experience_survey, project)
diff --git a/app/finders/concerns/updated_at_filter.rb b/app/finders/concerns/updated_at_filter.rb
index 2d6bd7bf9f3..0e9a3fb5e8c 100644
--- a/app/finders/concerns/updated_at_filter.rb
+++ b/app/finders/concerns/updated_at_filter.rb
@@ -2,8 +2,12 @@
module UpdatedAtFilter
def by_updated_at(items)
- items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
- items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
+ updated_before = params[:updated_before]&.in_time_zone
+ updated_after = params[:updated_after]&.in_time_zone
+ return items.none if [updated_before, updated_after].all?(&:present?) && updated_before < updated_after
+
+ items = items.updated_before(updated_before) if updated_before.present?
+ items = items.updated_after(updated_after) if updated_after.present?
items
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 401bc473216..57a9538db15 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -31,6 +31,7 @@
#
class ProjectsFinder < UnionFinder
include CustomAttributesFilter
+ include UpdatedAtFilter
attr_accessor :params
attr_reader :current_user, :project_ids_relation
@@ -87,6 +88,7 @@ class ProjectsFinder < UnionFinder
collection = by_last_activity_before(collection)
collection = by_language(collection)
collection = by_feature_availability(collection)
+ collection = by_updated_at(collection)
by_repository_storage(collection)
end
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index 72372ae6b42..c725f165682 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -55,6 +55,10 @@ module Resolvers
required: false,
description: 'Limit result to draft merge requests.'
+ argument :approved, GraphQL::Types::Boolean,
+ required: false,
+ description: 'Limit results to approved merge requests.'
+
argument :created_after, Types::TimeType,
required: false,
description: 'Merge requests created after this timestamp.'
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 063eef41f77..2157b3044eb 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -7,6 +7,7 @@ module IdeHelper
'can-use-new-web-ide' => can_use_new_web_ide?.to_s,
'use-new-web-ide' => use_new_web_ide?.to_s,
'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
+ 'sign-in-path' => new_session_path(current_user),
'user-preferences-path' => profile_preferences_path,
'editor-font-src-url' => font_url('jetbrains-mono/JetBrainsMono.woff2'),
'editor-font-family' => 'JetBrains Mono',
diff --git a/app/models/project.rb b/app/models/project.rb
index b2839ad78a3..538689b6efc 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -42,6 +42,7 @@ class Project < ApplicationRecord
include BlocksUnsafeSerialization
include Subquery
include IssueParent
+ include UpdatedAtFilterable
extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override
diff --git a/app/models/work_items/widgets/base.rb b/app/models/work_items/widgets/base.rb
index 3a5b03bd514..b54b84f1e1b 100644
--- a/app/models/work_items/widgets/base.rb
+++ b/app/models/work_items/widgets/base.rb
@@ -15,6 +15,12 @@ module WorkItems
[]
end
+ def self.callback_class
+ Issuable::Callbacks.const_get(name.demodulize, false)
+ rescue NameError
+ nil
+ end
+
def type
self.class.type
end
diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb
index ac019d9ec5b..80293771b5f 100644
--- a/app/services/bulk_imports/create_service.rb
+++ b/app/services/bulk_imports/create_service.rb
@@ -27,6 +27,11 @@
#
module BulkImports
class CreateService
+ ENTITY_TYPES_MAPPING = {
+ 'group_entity' => 'groups',
+ 'project_entity' => 'projects'
+ }.freeze
+
attr_reader :current_user, :params, :credentials
def initialize(current_user, params, credentials)
@@ -57,6 +62,7 @@ module BulkImports
def validate!
client.validate_instance_version!
+ validate_setting_enabled!
client.validate_import_scopes!
end
@@ -88,6 +94,28 @@ module BulkImports
end
end
+ def validate_setting_enabled!
+ source_full_path, source_type = Array.wrap(params)[0].values_at(:source_full_path, :source_type)
+ entity_type = ENTITY_TYPES_MAPPING.fetch(source_type)
+ if source_full_path =~ /^[0-9]+$/
+ query = query_type(entity_type)
+ response = graphql_client.execute(
+ graphql_client.parse(query.to_s),
+ { full_path: source_full_path }
+ ).original_hash
+
+ source_entity_identifier = ::GlobalID.parse(response.dig(*query.data_path, 'id')).model_id
+ else
+ source_entity_identifier = ERB::Util.url_encode(source_full_path)
+ end
+
+ client.get("/#{entity_type}/#{source_entity_identifier}/export_relations/status")
+ # the source instance will return a 404 if the feature is disabled as the endpoint won't be available
+ rescue Gitlab::HTTP::BlockedUrlError
+ rescue BulkImports::NetworkError
+ raise ::BulkImports::Error.setting_not_enabled
+ end
+
def track_access_level(entity_params)
Gitlab::Tracking.event(
self.class.name,
@@ -140,5 +168,20 @@ module BulkImports
token: @credentials[:access_token]
)
end
+
+ def graphql_client
+ @graphql_client ||= BulkImports::Clients::Graphql.new(
+ url: @credentials[:url],
+ token: @credentials[:access_token]
+ )
+ end
+
+ def query_type(entity_type)
+ if entity_type == 'groups'
+ BulkImports::Groups::Graphql::GetGroupQuery.new(context: nil)
+ else
+ BulkImports::Projects::Graphql::GetProjectQuery.new(context: nil)
+ end
+ end
end
end
diff --git a/app/services/concerns/work_items/widgetable_service.rb b/app/services/concerns/work_items/widgetable_service.rb
index 24ade9336b2..965d1cee55d 100644
--- a/app/services/concerns/work_items/widgetable_service.rb
+++ b/app/services/concerns/work_items/widgetable_service.rb
@@ -2,6 +2,21 @@
module WorkItems
module WidgetableService
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def initialize_callbacks!(work_item)
+ @callbacks = work_item.widgets.filter_map do |widget|
+ callback_class = widget.class.try(:callback_class)
+ callback_params = @widget_params[widget.class.api_symbol]
+
+ next if callback_class.nil? || callback_params.blank?
+
+ callback_class.new(issuable: work_item, current_user: current_user, params: callback_params)
+ end
+
+ @callbacks.each(&:after_initialize)
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
def execute_widgets(work_item:, callback:, widget_params: {}, service_params: {})
work_item.widgets.each do |widget|
widget_service(widget, service_params).try(callback, params: widget_params[widget.class.api_symbol])
diff --git a/app/services/issuable/callbacks/base.rb b/app/services/issuable/callbacks/base.rb
new file mode 100644
index 00000000000..483013b4eab
--- /dev/null
+++ b/app/services/issuable/callbacks/base.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Issuable
+ module Callbacks
+ class Base
+ include Gitlab::Allowable
+
+ def initialize(issuable:, current_user:, params:)
+ @issuable = issuable
+ @current_user = current_user
+ @params = params
+ end
+
+ def after_initialize; end
+ def after_update_commit; end
+ def after_save_commit; end
+
+ private
+
+ attr_reader :issuable, :current_user, :params
+
+ def has_permission?(permission)
+ can?(current_user, permission, issuable)
+ end
+ end
+ end
+end
diff --git a/app/services/issuable/callbacks/milestone.rb b/app/services/issuable/callbacks/milestone.rb
new file mode 100644
index 00000000000..44c276152f5
--- /dev/null
+++ b/app/services/issuable/callbacks/milestone.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Issuable
+ module Callbacks
+ class Milestone < Base
+ ALLOWED_PARAMS = %i[milestone milestone_id skip_milestone_email].freeze
+
+ def after_initialize
+ return unless params.key?(:milestone_id) && has_permission?(:"set_#{issuable.to_ability_name}_metadata")
+
+ @old_milestone = issuable.milestone
+
+ if params[:milestone_id].blank? || params[:milestone_id] == IssuableFinder::Params::NONE
+ issuable.milestone = nil
+
+ return
+ end
+
+ resource_group = issuable.project&.group || issuable.try(:namespace)
+ project_ids = [issuable.project&.id].compact
+
+ milestone = MilestonesFinder.new({
+ project_ids: project_ids,
+ group_ids: resource_group&.self_and_ancestors&.select(:id),
+ ids: [params[:milestone_id]]
+ }).execute.first
+
+ issuable.milestone = milestone if milestone
+ end
+
+ def after_update_commit
+ return unless issuable.previous_changes.include?('milestone_id')
+
+ update_usage_data_counters
+ send_milestone_change_notification
+
+ GraphqlTriggers.issuable_milestone_updated(issuable)
+ end
+
+ def after_save_commit
+ return unless issuable.previous_changes.include?('milestone_id')
+
+ invalidate_milestone_counters
+ end
+
+ private
+
+ def invalidate_milestone_counters
+ [@old_milestone, issuable.milestone].compact.each do |milestone|
+ case issuable
+ when Issue
+ ::Milestones::ClosedIssuesCountService.new(milestone).delete_cache
+ ::Milestones::IssuesCountService.new(milestone).delete_cache
+ when MergeRequest
+ ::Milestones::MergeRequestsCountService.new(milestone).delete_cache
+ end
+ end
+ end
+
+ def update_usage_data_counters
+ return unless issuable.is_a?(MergeRequest)
+
+ Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
+ .track_milestone_changed_action(user: current_user)
+ end
+
+ def send_milestone_change_notification
+ return if params[:skip_milestone_email]
+
+ notification_service = NotificationService.new.async
+
+ if issuable.milestone.nil?
+ notification_service.removed_milestone(issuable, current_user)
+ else
+ notification_service.changed_milestone(issuable, issuable.milestone, current_user)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index c630d01cd84..ecfe805a14d 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -3,6 +3,31 @@
class IssuableBaseService < ::BaseContainerService
private
+ def available_callbacks
+ [
+ Issuable::Callbacks::Milestone
+ ].freeze
+ end
+
+ def initialize_callbacks!(issuable)
+ @callbacks = available_callbacks.filter_map do |callback_class|
+ callback_params = params.slice(*callback_class::ALLOWED_PARAMS)
+
+ next if callback_params.empty?
+
+ callback_class.new(issuable: issuable, current_user: current_user, params: callback_params)
+ end
+
+ remove_callback_params
+ @callbacks.each(&:after_initialize)
+ end
+
+ def remove_callback_params
+ available_callbacks.each do |callback_class|
+ callback_class::ALLOWED_PARAMS.each { |p| params.delete(p) }
+ end
+ end
+
def self.constructor_container_arg(value)
# TODO: Dynamically determining the type of a constructor arg based on the class is an antipattern,
# but the root cause is that Epics::BaseService has some issues that inheritance may not be the
@@ -13,14 +38,12 @@ class IssuableBaseService < ::BaseContainerService
{ container: value }
end
- attr_accessor :params, :skip_milestone_email
+ attr_accessor :params
def initialize(container:, current_user: nil, params: {})
# we need to exclude project params since they may come from external requests. project should always
# be passed as part of the service's initializer
super(container: container, current_user: current_user, params: params.except(:project, :project_id))
-
- @skip_milestone_email = @params.delete(:skip_milestone_email)
end
def can_admin_issuable?(issuable)
@@ -36,10 +59,7 @@ class IssuableBaseService < ::BaseContainerService
end
def filter_params(issuable)
- params.delete(:milestone)
-
unless can_set_issuable_metadata?(issuable)
- params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
params.delete(:add_labels)
@@ -63,7 +83,6 @@ class IssuableBaseService < ::BaseContainerService
params.delete(:remove_contacts) unless can?(current_user, :set_issue_crm_contacts, issuable)
filter_assignees(issuable)
- filter_milestone
filter_labels
filter_severity(issuable)
filter_escalation_status(issuable)
@@ -104,19 +123,6 @@ class IssuableBaseService < ::BaseContainerService
can?(user, ability_name, resource)
end
- def filter_milestone
- milestone_id = params[:milestone_id]
- return unless milestone_id
-
- params[:milestone_id] = '' if milestone_id == IssuableFinder::Params::NONE
- groups = project.group&.self_and_ancestors&.select(:id)
-
- milestone =
- Milestone.for_projects_and_groups([project.id], groups).find_by_id(milestone_id)
-
- params[:milestone_id] = '' unless milestone
- end
-
def filter_labels
label_ids_to_filter(:add_label_ids, :add_labels, false)
label_ids_to_filter(:remove_label_ids, :remove_labels, true)
@@ -208,6 +214,8 @@ class IssuableBaseService < ::BaseContainerService
end
def create(issuable, skip_system_notes: false)
+ initialize_callbacks!(issuable)
+
handle_quick_actions(issuable)
filter_params(issuable)
@@ -231,6 +239,8 @@ class IssuableBaseService < ::BaseContainerService
end
if issuable_saved
+ @callbacks.each(&:after_save_commit)
+
create_system_notes(issuable, is_update: false) unless skip_system_notes
handle_changes(issuable, { params: params })
@@ -280,6 +290,8 @@ class IssuableBaseService < ::BaseContainerService
end
def update(issuable)
+ initialize_callbacks!(issuable)
+
prepare_update_params(issuable)
handle_quick_actions(issuable)
filter_params(issuable)
@@ -292,7 +304,7 @@ class IssuableBaseService < ::BaseContainerService
assign_requested_crm_contacts(issuable)
widget_params = filter_widget_params
- if issuable.changed? || params.present? || widget_params.present?
+ if issuable.changed? || params.present? || widget_params.present? || @callbacks.present?
issuable.assign_attributes(allowed_update_params(params))
if issuable.description_changed?
@@ -309,13 +321,15 @@ class IssuableBaseService < ::BaseContainerService
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
update_project_counters = issuable.project && update_project_counter_caches?(issuable)
- ensure_milestone_available(issuable)
issuable_saved = issuable.with_transaction_returning_status do
transaction_update(issuable, { save_with_touch: should_touch })
end
if issuable_saved
+ @callbacks.each(&:after_update_commit)
+ @callbacks.each(&:after_save_commit)
+
create_system_notes(
issuable, old_labels: old_associations[:labels], old_milestone: old_associations[:milestone]
)
@@ -586,14 +600,6 @@ class IssuableBaseService < ::BaseContainerService
project
end
- # we need to check this because milestone from milestone_id param is displayed on "new" page
- # where private project milestone could leak without this check
- def ensure_milestone_available(issuable)
- return unless issuable.supports_milestone? && issuable.milestone_id.present?
-
- issuable.milestone_id = nil unless issuable.milestone_available?
- end
-
def update_timestamp?(issuable)
issuable.changes.keys != ["relative_position"]
end
diff --git a/app/services/issues/after_create_service.rb b/app/services/issues/after_create_service.rb
index 5d10eca2979..e996724ebd6 100644
--- a/app/services/issues/after_create_service.rb
+++ b/app/services/issues/after_create_service.rb
@@ -4,7 +4,6 @@ module Issues
class AfterCreateService < Issues::BaseService
def execute(issue)
todo_service.new_issue(issue, current_user)
- delete_milestone_total_issue_counter_cache(issue.milestone)
track_incident_action(current_user, issue, :incident_created)
end
end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 6b219dd429a..6367bdc5f80 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -121,18 +121,6 @@ module Issues
super || issue.confidential_changed?
end
- def delete_milestone_closed_issue_counter_cache(milestone)
- return unless milestone
-
- Milestones::ClosedIssuesCountService.new(milestone).delete_cache
- end
-
- def delete_milestone_total_issue_counter_cache(milestone)
- return unless milestone
-
- Milestones::IssuesCountService.new(milestone).delete_cache
- end
-
override :allowed_create_params
def allowed_create_params(params)
super(params).except(:work_item_type_id, :work_item_type)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 606cad35c68..1b87c42be69 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -4,7 +4,7 @@ module Issues
class BuildService < Issues::BaseService
include ResolveDiscussions
- def execute
+ def execute(initialize_callbacks: true)
filter_resolve_discussion_params
container_param = case container
@@ -17,7 +17,7 @@ module Issues
end
@issue = model_klass.new(issue_params.merge(container_param)).tap do |issue|
- ensure_milestone_available(issue)
+ initialize_callbacks!(issue) if initialize_callbacks
end
end
@@ -100,7 +100,6 @@ module Issues
params[:work_item_type] = WorkItems::Type.find_by(id: params[:work_item_type_id]) if params[:work_item_type_id].present? # rubocop: disable CodeReuse/ActiveRecord
- public_issue_params << :milestone_id if can?(current_user, :admin_issue, container)
public_issue_params << :issue_type if create_issue_type_allowed?(container, params[:issue_type])
base_type = params[:work_item_type]&.base_type
public_issue_params << :work_item_type if create_issue_type_allowed?(container, base_type)
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 4f6a859e20e..87e27ef2763 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -43,7 +43,7 @@ module Issues
Onboarding::ProgressService.new(project.namespace).execute(action: :issue_auto_closed)
end
- delete_milestone_closed_issue_counter_cache(issue.milestone)
+ Milestones::ClosedIssuesCountService.new(issue.milestone).delete_cache if issue.milestone
end
issue
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index d8e5800e1aa..e652fec02e5 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -24,7 +24,10 @@ module Issues
def execute(skip_system_notes: false)
return error(_('Operation not allowed'), 403) unless @current_user.can?(authorization_action, container)
- @issue = @build_service.execute
+ # We should not initialize the callback classes during the build service execution because these will be
+ # initialized when we call #create below
+ @issue = @build_service.execute(initialize_callbacks: false)
+
# issue_type is set in BuildService, so we can delete it from params, in later phase
# it can be set also from quick actions - in that case work_item_id is synced later again
params.delete(:issue_type)
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index f4f81e9455a..3330c462947 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -13,7 +13,7 @@ module Issues
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
- delete_milestone_closed_issue_counter_cache(issue.milestone)
+ Milestones::ClosedIssuesCountService.new(issue.milestone).delete_cache if issue.milestone
track_incident_action(current_user, issue, :incident_reopened)
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 322065c5b7c..b64826d5357 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -64,7 +64,6 @@ module Issues
handle_assignee_changes(issue, old_assignees)
handle_confidential_change(issue)
handle_added_labels(issue, old_labels)
- handle_milestone_change(issue)
handle_added_mentions(issue, old_mentioned_users)
handle_severity_change(issue, old_severity)
handle_escalation_status_change(issue)
@@ -166,35 +165,6 @@ module Issues
end
end
- def handle_milestone_change(issue)
- return unless issue.previous_changes.include?('milestone_id')
-
- invalidate_milestone_issue_counters(issue)
- send_milestone_change_notification(issue)
- GraphqlTriggers.issuable_milestone_updated(issue)
- end
-
- def invalidate_milestone_issue_counters(issue)
- issue.previous_changes['milestone_id'].each do |milestone_id|
- next unless milestone_id
-
- milestone = Milestone.find_by_id(milestone_id)
-
- delete_milestone_closed_issue_counter_cache(milestone)
- delete_milestone_total_issue_counter_cache(milestone)
- end
- end
-
- def send_milestone_change_notification(issue)
- return if skip_milestone_email
-
- if issue.milestone.nil?
- notification_service.async.removed_milestone(issue, current_user)
- else
- notification_service.async.changed_milestone(issue, issue.milestone, current_user)
- end
- end
-
def handle_added_mentions(issue, old_mentioned_users)
added_mentions = issue.mentioned_users(current_user) - old_mentioned_users
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index 11251e56ee3..f3b1c663fa2 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -39,8 +39,6 @@ module MergeRequests
Gitlab::UsageDataCounters::MergeRequestCounter.count(:create)
link_lfs_objects(merge_request)
-
- delete_milestone_total_merge_requests_counter_cache(merge_request.milestone)
end
def link_lfs_objects(merge_request)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 97ca96043fb..0d59e442dce 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -263,12 +263,6 @@ module MergeRequests
merge_request.update(merge_error: message) if save_message_on_model
end
- def delete_milestone_total_merge_requests_counter_cache(milestone)
- return unless milestone
-
- Milestones::MergeRequestsCountService.new(milestone).delete_cache
- end
-
def trigger_merge_request_reviewers_updated(merge_request)
GraphqlTriggers.merge_request_reviewers_updated(merge_request)
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index b9a681f29db..d5b109a764d 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -16,6 +16,8 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
+ initialize_callbacks!(merge_request)
+
process_params
merge_request.compare_commits = []
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 255d96f4969..642cffa6c0d 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -36,7 +36,6 @@ module MergeRequests
end
handle_target_branch_change(merge_request)
- handle_milestone_change(merge_request)
handle_draft_status_change(merge_request, changed_fields)
track_title_and_desc_edits(changed_fields)
@@ -204,25 +203,6 @@ module MergeRequests
)
end
- def handle_milestone_change(merge_request)
- return if skip_milestone_email
-
- return unless merge_request.previous_changes.include?('milestone_id')
-
- merge_request_activity_counter.track_milestone_changed_action(user: current_user)
-
- previous_milestone = Milestone.find_by_id(merge_request.previous_changes['milestone_id'].first)
- delete_milestone_total_merge_requests_counter_cache(previous_milestone)
-
- if merge_request.milestone.nil?
- notification_service.async.removed_milestone(merge_request, current_user)
- else
- notification_service.async.changed_milestone(merge_request, merge_request.milestone, current_user)
-
- delete_milestone_total_merge_requests_counter_cache(merge_request.milestone)
- end
- end
-
def create_branch_change_note(issuable, branch_type, event_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type, event_type,
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index dd074f7472b..b64771e4c58 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -3,15 +3,27 @@ module Packages
module Npm
class CreatePackageService < ::Packages::CreatePackageService
include Gitlab::Utils::StrongMemoize
+ include ExclusiveLeaseGuard
PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename].freeze
+ DEFAULT_LEASE_TIMEOUT = 1.hour.to_i
def execute
return error('Version is empty.', 400) if version.blank?
return error('Package already exists.', 403) if current_package_exists?
return error('File is too large.', 400) if file_size_exceeded?
- ApplicationRecord.transaction { create_npm_package! }
+ if Feature.enabled?(:npm_obtain_lease_to_create_package, project)
+ package = try_obtain_lease do
+ ApplicationRecord.transaction { create_npm_package! }
+ end
+
+ return error('Could not obtain package lease.', 400) unless package
+
+ package
+ else
+ ApplicationRecord.transaction { create_npm_package! }
+ end
end
private
@@ -103,6 +115,16 @@ module Packages
def file_size_exceeded?
project.actual_limits.exceeded?(:npm_max_file_size, calculated_package_file_size)
end
+
+ # used by ExclusiveLeaseGuard
+ def lease_key
+ "packages:npm:create_package_service:packages:#{project.id}_#{name}"
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_timeout
+ DEFAULT_LEASE_TIMEOUT
+ end
end
end
end
diff --git a/app/services/tasks_to_be_done/base_service.rb b/app/services/tasks_to_be_done/base_service.rb
index ba52e9abeb2..1c74e803e0b 100644
--- a/app/services/tasks_to_be_done/base_service.rb
+++ b/app/services/tasks_to_be_done/base_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module TasksToBeDone
- class BaseService < ::IssuableBaseService
+ class BaseService < ::BaseContainerService
LABEL_PREFIX = 'tasks to be done'
def initialize(container:, current_user:, assignee_ids: [])
@@ -19,8 +19,8 @@ module TasksToBeDone
update_service = Issues::UpdateService.new(container: project, current_user: current_user, params: { add_assignee_ids: params[:assignee_ids] })
update_service.execute(issue)
else
- build_service = Issues::BuildService.new(container: project, current_user: current_user, params: params)
- create(build_service.execute)
+ create_service = Issues::CreateService.new(container: project, current_user: current_user, params: params, spam_params: nil)
+ create_service.execute
end
end
diff --git a/app/services/work_items/widgets/milestone_service/create_service.rb b/app/services/work_items/widgets/milestone_service/create_service.rb
deleted file mode 100644
index e8d6bfe503c..00000000000
--- a/app/services/work_items/widgets/milestone_service/create_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module WorkItems
- module Widgets
- module MilestoneService
- class CreateService < WorkItems::Widgets::MilestoneService::BaseService
- def before_create_callback(params:)
- handle_milestone_change(params: params)
- end
- end
- end
- end
-end
diff --git a/app/services/work_items/widgets/milestone_service/update_service.rb b/app/services/work_items/widgets/milestone_service/update_service.rb
deleted file mode 100644
index 7ff0c2a5367..00000000000
--- a/app/services/work_items/widgets/milestone_service/update_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module WorkItems
- module Widgets
- module MilestoneService
- class UpdateService < WorkItems::Widgets::MilestoneService::BaseService
- def before_update_callback(params:)
- handle_milestone_change(params: params)
- end
- end
- end
- end
-end
diff --git a/config/feature_flags/development/deprecate_vulnerabilities_feedback.yml b/config/feature_flags/development/deprecate_vulnerabilities_feedback.yml
new file mode 100644
index 00000000000..c3d68605571
--- /dev/null
+++ b/config/feature_flags/development/deprecate_vulnerabilities_feedback.yml
@@ -0,0 +1,8 @@
+---
+name: deprecate_vulnerabilities_feedback
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86497
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/361797
+milestone: '15.0'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/config/feature_flags/development/npm_obtain_lease_to_create_package.yml b/config/feature_flags/development/npm_obtain_lease_to_create_package.yml
new file mode 100644
index 00000000000..949b2e0dfde
--- /dev/null
+++ b/config/feature_flags/development/npm_obtain_lease_to_create_package.yml
@@ -0,0 +1,8 @@
+---
+name: npm_obtain_lease_to_create_package
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114317
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397668
+milestone: '15.11'
+type: development
+group: group::package registry
+default_enabled: false
diff --git a/doc/administration/geo/replication/img/adding_a_secondary_v13_3.png b/doc/administration/geo/replication/img/adding_a_secondary_v13_3.png
deleted file mode 100644
index e43ace99666..00000000000
--- a/doc/administration/geo/replication/img/adding_a_secondary_v13_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 986779fce52..a2d70dfaeef 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -14875,6 +14875,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="groupmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="groupmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="groupmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="groupmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
@@ -16205,6 +16206,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestassigneeassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestassigneeassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestassigneeassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestassigneeassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -16239,6 +16241,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestassigneeauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestassigneeauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestassigneeauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestassigneeauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -16290,6 +16293,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestassigneereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestassigneereviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestassigneereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestassigneereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
@@ -16452,6 +16456,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestauthorassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestauthorassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestauthorassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestauthorassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -16486,6 +16491,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestauthorauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestauthorauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestauthorauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestauthorauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -16537,6 +16543,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestauthorreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestauthorreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestauthorreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestauthorreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
@@ -16718,6 +16725,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestparticipantassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestparticipantassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestparticipantassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestparticipantassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -16752,6 +16760,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestparticipantauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestparticipantauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestparticipantauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestparticipantauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -16803,6 +16812,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestparticipantreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestparticipantreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
@@ -16984,6 +16994,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestreviewerassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestreviewerassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestreviewerassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestreviewerassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -17018,6 +17029,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestreviewerauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestreviewerauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestreviewerauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="mergerequestreviewerauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -17069,6 +17081,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mergerequestreviewerreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestreviewerreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
@@ -19032,6 +19045,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="projectmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="projectmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="projectmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="projectmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
@@ -21299,6 +21313,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="usercoreassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="usercoreassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="usercoreassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="usercoreassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -21333,6 +21348,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="usercoreauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="usercoreauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="usercoreauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="usercoreauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -21384,6 +21400,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="usercorereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="usercorereviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="usercorereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="usercorereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
@@ -25884,6 +25901,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="userassignedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="userassignedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="userassignedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="userassignedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -25918,6 +25936,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="userauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="userauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="userauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
| <a id="userauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before this timestamp. |
@@ -25969,6 +25988,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="userreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. |
| <a id="userreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="userreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="userreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after this timestamp. |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index e1397fcb640..1fbe65d903e 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -45,6 +45,7 @@ Supported attributes:
| ------------------------------- | -------------- | -------- | ----------- |
| `approved_by_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `id`. Maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `approver_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have specified all the users with the given `id` as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
+| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. |
| `assignee_id` | integer | **{dotted-circle}** No | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `author_id` | integer | **{dotted-circle}** No | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | **{dotted-circle}** No | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. |
@@ -248,6 +249,7 @@ Supported attributes:
| `id` | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
| `approved_by_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `id`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `approver_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have specified all the users with the given `id` as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
+| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. |
| `assignee_id` | integer | **{dotted-circle}** No | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `author_id` | integer | **{dotted-circle}** No | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. |
| `author_username` | string | **{dotted-circle}** No | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. |
@@ -438,6 +440,7 @@ Supported attributes:
| `approved_by_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `id`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `approved_by_usernames` **(PREMIUM)** | string array | **{dotted-circle}** No | Returns merge requests which have been approved by all the users with the given `username`, with a maximum of 5. `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
| `approver_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
+| `approved` | string | **{dotted-circle}** No | Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3159) in GitLab 15.11. |
| `assignee_id` | integer | **{dotted-circle}** No | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `author_id` | integer | **{dotted-circle}** No | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. |
| `author_username` | string | **{dotted-circle}** No | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 3cd7cdd811d..16b3bf95bd7 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -67,6 +67,8 @@ GET /projects
| `with_issues_enabled` | boolean | **{dotted-circle}** No | Limit by enabled issues feature. |
| `with_merge_requests_enabled` | boolean | **{dotted-circle}** No | Limit by enabled merge requests feature. |
| `with_programming_language` | string | **{dotted-circle}** No | Limit by projects which use the given programming language. |
+| `updated_before` | datetime | **{dotted-circle}** No | Limit results to projects last updated before the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. For this filter to work, you must also provide `updated_at` as the `order_by` attribute. |
+| `updated_after` | datetime | **{dotted-circle}** No | Limit results to projects last updated after the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. For this filter to work, you must also provide `updated_at` as the `order_by` attribute. |
This endpoint supports [keyset pagination](rest/index.md#keyset-based-pagination)
for selected `order_by` options.
@@ -341,6 +343,8 @@ GET /users/:user_id/projects
| `with_issues_enabled` | boolean | **{dotted-circle}** No | Limit by enabled issues feature. |
| `with_merge_requests_enabled` | boolean | **{dotted-circle}** No | Limit by enabled merge requests feature. |
| `with_programming_language` | string | **{dotted-circle}** No | Limit by projects which use the given programming language. |
+| `updated_before` | datetime | **{dotted-circle}** No | Limit results to projects last updated before the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. |
+| `updated_after` | datetime | **{dotted-circle}** No | Limit results to projects last updated after the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. |
```json
[
@@ -607,6 +611,8 @@ GET /users/:user_id/starred_projects
| `with_custom_attributes` | boolean | **{dotted-circle}** No | Include [custom attributes](custom_attributes.md) in response. _(administrator only)_ |
| `with_issues_enabled` | boolean | **{dotted-circle}** No | Limit by enabled issues feature. |
| `with_merge_requests_enabled` | boolean | **{dotted-circle}** No | Limit by enabled merge requests feature. |
+| `updated_before` | datetime | **{dotted-circle}** No | Limit results to projects last updated before the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. |
+| `updated_after` | datetime | **{dotted-circle}** No | Limit results to projects last updated after the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/5/starred_projects"
@@ -1568,6 +1574,8 @@ GET /projects/:id/forks
| `with_custom_attributes` | boolean | **{dotted-circle}** No | Include [custom attributes](custom_attributes.md) in response. _(administrators only)_ |
| `with_issues_enabled` | boolean | **{dotted-circle}** No | Limit by enabled issues feature. |
| `with_merge_requests_enabled` | boolean | **{dotted-circle}** No | Limit by enabled merge requests feature. |
+| `updated_before` | datetime | **{dotted-circle}** No | Limit results to projects last updated before the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. |
+| `updated_after` | datetime | **{dotted-circle}** No | Limit results to projects last updated after the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/forks"
diff --git a/doc/development/documentation/topic_types/glossary.md b/doc/development/documentation/topic_types/glossary.md
new file mode 100644
index 00000000000..4985101a391
--- /dev/null
+++ b/doc/development/documentation/topic_types/glossary.md
@@ -0,0 +1,70 @@
+---
+stage: none
+group: Style Guide
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Glossary topic type
+
+A glossary provides a list of unfamiliar terms and their definitions to help users understand a specific
+GitLab feature.
+
+Each glossary item provides a single term and its associated definition. The definition should answer the questions:
+
+- **What** is this?
+- **Why** would you use it?
+
+For glossary terms:
+
+- Do not use jargon.
+- Do not use internal terminology or acronyms.
+- Ensure the correct usage is defined in the [word list](../styleguide/word_list.md).
+
+## Alternatives to glossaries
+
+Glossaries should provide short, concise term-definition pairs.
+
+- If a definition requires more than a brief explanation, use a concept topic instead.
+- If you find yourself explaining how to use the feature, use a task topic instead.
+
+## Glossary example
+
+Glossary topics should be in this format. Use an unordered list primarily. You can use a table if you need to apply
+additional categorization.
+
+Try to include glossary topics on pages that explain the feature, rather than as a standalone page.
+
+```markdown
+## FeatureName glossary
+
+This glossary provides definitions for terms related to FeatureName.
+
+- **Term A**: Term A does this thing.
+- **Term B**: Term B does this thing.
+- **Term C**: Term C does this thing.
+```
+
+If you use the table format:
+
+```markdown
+## FeatureName glossary
+
+This glossary provides definitions for terms related to FeatureName.
+
+| Term | Definition | Additional category |
+|--------|-------------------------|---------------------|
+| Term A | Term A does this thing. | |
+| Term B | Term B does this thing. | |
+| Term C | Term C does this thing. | |
+```
+
+## Glossary topic titles
+
+Use `FeatureName glossary`.
+
+Don't use alternatives to `glossary`. For example:
+
+- `Terminology`
+- `Glossary of terms`
+- `Glossary of common terms`
+- `Definitions`
diff --git a/doc/development/documentation/topic_types/index.md b/doc/development/documentation/topic_types/index.md
index cfc231c268a..6f0786a6a52 100644
--- a/doc/development/documentation/topic_types/index.md
+++ b/doc/development/documentation/topic_types/index.md
@@ -19,8 +19,11 @@ includes a task or reference topic.
The tech writing team sometimes uses the acronym `CTRT` to refer to our topic types.
The acronym refers to the first letter of each topic type.
-In addition to the four primary topic types, we also have a page type for
-[Tutorials](tutorial.md) and [Get started](#get-started).
+In addition to the four primary topic types, we also have:
+
+- [Tutorials](tutorial.md) (a page type)
+- [Get started](#get-started) (a page type)
+- [Glossaries](glossary.md) (a topic or page type)
## Related topics
diff --git a/doc/development/stage_group_observability/dashboards/img/error_budget_detail_sli_detail.png b/doc/development/stage_group_observability/dashboards/img/error_budget_detail_sli_detail.png
deleted file mode 100644
index 99530886ae9..00000000000
--- a/doc/development/stage_group_observability/dashboards/img/error_budget_detail_sli_detail.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/work_items_widgets.md b/doc/development/work_items_widgets.md
index 5b9602595bb..bafceccdafe 100644
--- a/doc/development/work_items_widgets.md
+++ b/doc/development/work_items_widgets.md
@@ -137,3 +137,27 @@ Each record in the table defines mapping of a widget to a work item type. Curren
| 4 | 1 | 1 | 1 | 0 | MyWeight | false |
| 5 | 2 | 0 | 1 | 1 | Other Weight | false |
| 6 | 3 | 0 | 1 | 1 | Weight | true |
+
+## Backend architecture
+
+You can update widgets using custom fine-grained mutations (for example, `WorkItemCreateFromTask`) or as part of the
+`workItemCreate` or `workItemUpdate` mutations.
+
+### Widget callbacks
+
+When updating the widget together with the work item's mutation, backend code should be implemented using
+callback classes that inherit from `Issuable::Callbacks::Base`. These classes have callback methods
+that are named similar to ActiveRecord callbacks and behave similarly.
+
+Callback classes with the same name as the widget are automatically used. For example, `Issuable::Callbacks::Milestone`
+is called when the work item has the milestone widget. To use a different class, you can override the `callback_class`
+class method.
+
+#### Available callbacks
+
+- `after_initialize` is called after the work item is initialized by the `BuildService` and before
+ the work item is saved by the `CreateService` and `UpdateService`. This callback runs outside the
+ creation or update database transaction.
+- `after_update_commit` is called after the DB update transaction is committed by the `UpdateService`.
+- `after_save_commit` is called after the creation or DB update transaction is committed by the
+ `CreateService` or `UpdateService`.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 072e773b73f..7ac9c4c83a2 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -44,6 +44,9 @@ for additional guidance on information your identity provider may require.
GitLab provides the following information for guidance only.
If you have any questions on configuring the SAML app, contact your provider's support.
+If you are having issues setting up your identity provider, see the
+[troubleshooting documentation](#troubleshooting).
+
### Set up Azure
To set up SSO with Azure as your identity provider:
@@ -214,18 +217,7 @@ After you set up your identity provider to work with GitLab, you must configure
NOTE:
The certificate [fingerprint algorithm](../../../integration/saml.md#configure-saml-on-your-idp) must be in SHA1. When configuring the identity provider (such as [Google Workspace](#set-up-google-workspace)), use a secure signature algorithm.
-### Additional configuration information
-
-Many SAML terms can vary between providers. It is possible that the information you are looking for is listed under another name.
-
-For more information, start with your identity provider's documentation. Look for their options and examples to see how they configure SAML. This can provide hints on what you need to configure GitLab to work with these providers.
-
-It can also help to look at our [more detailed docs for self-managed GitLab](../../../integration/saml.md).
-SAML configuration for GitLab.com is mostly the same as for self-managed instances.
-However, self-managed GitLab instances use a configuration file that supports more options as described in the external [OmniAuth SAML documentation](https://github.com/omniauth/omniauth-saml/).
-Internally that uses the [`ruby-saml` library](https://github.com/onelogin/ruby-saml), so we sometimes check there to verify low level details of less commonly used options.
-
-It can also help to compare the XML response from your provider with our [example XML used for internal testing](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/spec/fixtures/saml/response.xml).
+If you are having issues configuring GitLab, see the [troubleshooting documentation](#troubleshooting).
### SSO enforcement
@@ -534,4 +526,18 @@ For more information on:
## Troubleshooting
-See our [troubleshooting SAML guide](troubleshooting.md).
+If you find it difficult to match the different SAML terms between GitLab and the
+identity provider:
+
+1. Check your identity provider's documentation. Look at their example SAML
+ configurations for information on the terms they use.
+1. Check the [SAML SSO for self-managed GitLab instances documentation](../../../integration/saml.md).
+ The self-managed GitLab instance SAML configuration file supports more options
+ than the GitLab.com file. You can find information on the self-managed instance
+ file in the:
+ - External [OmniAuth SAML documentation](https://github.com/omniauth/omniauth-saml/).
+ - [`ruby-saml` library](https://github.com/onelogin/ruby-saml).
+1. Compare the XML response from your provider with our
+ [example XML used for internal testing](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/spec/fixtures/saml/response.xml).
+
+For other troubleshooting information, see the [troubleshooting SAML guide](troubleshooting.md).
diff --git a/doc/user/img/observability_copy_shortened_link.png b/doc/user/img/observability_copy_shortened_link.png
deleted file mode 100644
index 217e56972cc..00000000000
--- a/doc/user/img/observability_copy_shortened_link.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index fe2e2acaae3..a26f027f329 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -273,7 +273,7 @@ feature flag after WebAuthn devices have been registered, these devices are not
On GitLab.com, WebAuthn devices are available.
FLAG:
-On self-managed GitLab, by default, optional one-time password authentication for WebAuthn devices is available. To hide the feature, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `webauthn_without_topt`.
+On self-managed GitLab, by default, optional one-time password authentication for WebAuthn devices is not available. To enable the feature, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `webauthn_without_totp`.
On GitLab.com, this feature is available.
WebAuthn is [supported by](https://caniuse.com/#search=webauthn) the following:
diff --git a/doc/user/project/merge_requests/reviews/img/custom_commit_v13_9.png b/doc/user/project/merge_requests/reviews/img/custom_commit_v13_9.png
deleted file mode 100644
index 170c04542dd..00000000000
--- a/doc/user/project/merge_requests/reviews/img/custom_commit_v13_9.png
+++ /dev/null
Binary files differ
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index aadcbe38b15..74c740f47cc 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -683,6 +683,8 @@ module API
finder_params[:user] = params.delete(:user) if params[:user]
finder_params[:id_after] = sanitize_id_param(params[:id_after]) if params[:id_after]
finder_params[:id_before] = sanitize_id_param(params[:id_before]) if params[:id_before]
+ finder_params[:updated_after] = declared_params[:updated_after] if declared_params[:updated_after]
+ finder_params[:updated_before] = declared_params[:updated_before] if declared_params[:updated_before]
finder_params
end
diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index ee3bb49c97f..0a0d70520ef 100644
--- a/lib/api/helpers/merge_requests_helpers.rb
+++ b/lib/api/helpers/merge_requests_helpers.rb
@@ -105,6 +105,9 @@ module API
documentation: { example: '2019-03-15T08:00:00Z' }
optional :environment, desc: 'Returns merge requests deployed to the given environment',
documentation: { example: '2019-03-15T08:00:00Z' }
+ optional :approved, type: String,
+ values: %w[yes no],
+ desc: 'Filters merge requests by their `approved` status. `yes` returns only approved merge requests. `no` returns only non-approved merge requests.'
end
params :optional_scope_param do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index b2e144ee165..697c2a7e214 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -40,6 +40,23 @@ module API
attrs.delete(:repository_storage) unless can?(current_user, :use_project_statistics_filters)
end
+ def validate_updated_at_order_and_filter!
+ return unless filter_by_updated_at? && provided_order_is_not_updated_at?
+
+ # This is necessary as not pairing this filter and ordering will produce an inneficient query
+ bad_request!('`updated_at` filter and `updated_at` sorting must be paired')
+ end
+
+ def provided_order_is_not_updated_at?
+ order_by_param = declared_params[:order_by]
+
+ order_by_param.present? && order_by_param.to_s != 'updated_at'
+ end
+
+ def filter_by_updated_at?
+ declared_params[:updated_before].present? || declared_params[:updated_after].present?
+ end
+
def verify_statistics_order_by_projects!
return unless Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS.include?(params[:order_by])
@@ -144,6 +161,8 @@ module API
optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
optional :topic, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of topics. Limit results to projects having all topics'
optional :topic_id, type: Integer, desc: 'Limit results to projects with the assigned topic given by the topic ID'
+ optional :updated_before, type: DateTime, desc: 'Return projects updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
+ optional :updated_after, type: DateTime, desc: 'Return projects updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
use :optional_filter_params_ee
end
@@ -261,6 +280,9 @@ module API
desc 'Get a list of visible projects for authenticated user' do
success code: 200, model: Entities::BasicProjectDetails
+ failure [
+ { code: 400, message: 'Bad request' }
+ ]
tags %w[projects]
is_array true
end
@@ -272,6 +294,7 @@ module API
# TODO: Set higher urgency https://gitlab.com/gitlab-org/gitlab/-/issues/211495
get feature_category: :projects, urgency: :low do
validate_projects_api_rate_limit_for_unauthenticated_users!
+ validate_updated_at_order_and_filter!
present_projects load_projects
end
diff --git a/lib/bulk_imports/clients/http.rb b/lib/bulk_imports/clients/http.rb
index 6efee83a0dd..616ab8754b4 100644
--- a/lib/bulk_imports/clients/http.rb
+++ b/lib/bulk_imports/clients/http.rb
@@ -165,6 +165,8 @@ module BulkImports
raise ::BulkImports::NetworkError.new("Unsuccessful response #{response.code} from #{response.request.path.path}. Body: #{response.parsed_response}", response: response)
+ rescue Gitlab::HTTP::BlockedUrlError => e
+ raise e
rescue *Gitlab::HTTP::HTTP_ERRORS => e
raise ::BulkImports::NetworkError, e
end
diff --git a/lib/bulk_imports/error.rb b/lib/bulk_imports/error.rb
index 009fa02a72a..fb72cb61de0 100644
--- a/lib/bulk_imports/error.rb
+++ b/lib/bulk_imports/error.rb
@@ -3,8 +3,7 @@
module BulkImports
class Error < StandardError
def self.unsupported_gitlab_version
- self.new("Unsupported GitLab version. Source instance must run GitLab version #{BulkImport::MIN_MAJOR_VERSION} " \
- "or later.")
+ self.new("Unsupported GitLab version. Minimum supported version is #{BulkImport::MIN_MAJOR_VERSION}.")
end
def self.scope_validation_failure
@@ -19,5 +18,11 @@ module BulkImports
def self.destination_full_path_validation_failure(full_path)
self.new("Import aborted as '#{full_path}' already exists. Change the destination and try again.")
end
+
+ def self.setting_not_enabled
+ self.new("Group import disabled on source or destination instance. " \
+ "Ask an administrator to enable it on both instances and try again."
+ )
+ end
end
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index fa5dd8c893c..85ca3d91a1a 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -504,7 +504,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
end
describe 'when user has made changes' do
- it 'shows a warning and can stay on page' do
+ it 'shows a warning and can stay on page', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/397683' do
content = 'new issue content'
find('body').send_keys('e')
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 297c6f84cef..13263698cfe 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe ProjectsFinder do
end
let_it_be(:internal_project) do
- create(:project, :internal, :merge_requests_disabled, group: group, name: 'B', path: 'B')
+ create(:project, :internal, :merge_requests_disabled, group: group, name: 'B', path: 'B', updated_at: 4.days.ago)
end
let_it_be(:public_project) do
@@ -133,6 +133,52 @@ RSpec.describe ProjectsFinder do
end
end
+ describe 'filter by updated_at' do
+ context 'when updated_before is present' do
+ let(:params) { { updated_before: 2.days.ago } }
+
+ it { is_expected.to contain_exactly(internal_project) }
+ end
+
+ context 'when updated_after is present' do
+ let(:params) { { updated_after: 2.days.ago } }
+
+ it { is_expected.not_to include(internal_project) }
+ end
+
+ context 'when both updated_before and updated_after are present' do
+ let(:params) { { updated_before: 2.days.ago, updated_after: 6.days.ago } }
+
+ it { is_expected.to contain_exactly(internal_project) }
+
+ context 'when updated_after > updated_before' do
+ let(:params) { { updated_after: 2.days.ago, updated_before: 6.days.ago } }
+
+ it { is_expected.to be_empty }
+
+ it 'does not query the DB' do
+ expect { subject.to_a }.to make_queries(0)
+ end
+ end
+
+ context 'when updated_after equals updated_before' do
+ let(:params) { { updated_after: internal_project.updated_at, updated_before: internal_project.updated_at } }
+
+ it 'allows an exact match' do
+ expect(subject).to contain_exactly(internal_project)
+ end
+ end
+
+ context 'when arguments are invalid datetimes' do
+ let(:params) { { updated_after: 'invalid', updated_before: 'inavlid' } }
+
+ it 'does not filter by updated_at' do
+ expect(subject).to contain_exactly(internal_project, public_project)
+ end
+ end
+ end
+ end
+
describe 'filter by tags (deprecated)' do
before do
public_project.reload
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 3067f3791c5..8ef41cc83f2 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -30,6 +30,11 @@ import { diffMetadata } from '../mock_data/diff_metadata';
jest.mock('~/alert');
+jest.mock('~/lib/utils/secret_detection', () => ({
+ confirmSensitiveAction: jest.fn(() => Promise.resolve(false)),
+ containsSensitiveToken: jest.requireActual('~/lib/utils/secret_detection').containsSensitiveToken,
+}));
+
describe('DiffsStoreActions', () => {
let mock;
@@ -926,31 +931,32 @@ describe('DiffsStoreActions', () => {
});
describe('saveDiffDiscussion', () => {
- it('dispatches actions', () => {
- const commitId = 'something';
- const formData = {
- diffFile: getDiffFileMock(),
- noteableData: {},
- };
- const note = {};
- const state = {
- commit: {
- id: commitId,
- },
- };
- const dispatch = jest.fn((name) => {
- switch (name) {
- case 'saveNote':
- return Promise.resolve({
- discussion: 'test',
- });
- case 'updateDiscussion':
- return Promise.resolve('discussion');
- default:
- return Promise.resolve({});
- }
- });
+ const dispatch = jest.fn((name) => {
+ switch (name) {
+ case 'saveNote':
+ return Promise.resolve({
+ discussion: 'test',
+ });
+ case 'updateDiscussion':
+ return Promise.resolve('discussion');
+ default:
+ return Promise.resolve({});
+ }
+ });
+ const commitId = 'something';
+ const formData = {
+ diffFile: getDiffFileMock(),
+ noteableData: {},
+ };
+ const note = {};
+ const state = {
+ commit: {
+ id: commitId,
+ },
+ };
+
+ it('dispatches actions', () => {
return diffActions.saveDiffDiscussion({ state, dispatch }, { note, formData }).then(() => {
expect(dispatch).toHaveBeenCalledTimes(5);
expect(dispatch).toHaveBeenNthCalledWith(1, 'saveNote', expect.any(Object), {
@@ -964,6 +970,16 @@ describe('DiffsStoreActions', () => {
expect(dispatch).toHaveBeenNthCalledWith(3, 'assignDiscussionsToDiff', ['discussion']);
});
});
+
+ it('should not add note with sensitive token', async () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ await diffActions.saveDiffDiscussion(
+ { state, dispatch },
+ { note: sensitiveMessage, formData },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ });
});
describe('toggleTreeOpen', () => {
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index bfc87f17092..2666f06e8d8 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -26,6 +26,7 @@ const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/publ
const TEST_FILE_PATH = 'foo/README.md';
const TEST_MR_ID = '7';
const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
+const TEST_SIGN_IN_PATH = 'sign-in';
const TEST_FORK_INFO = { fork_path: '/forky' };
const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
const TEST_START_REMOTE_PARAMS = {
@@ -56,6 +57,7 @@ describe('ide/init_gitlab_web_ide', () => {
el.dataset.editorFontSrcUrl = TEST_EDITOR_FONT_SRC_URL;
el.dataset.editorFontFormat = TEST_EDITOR_FONT_FORMAT;
el.dataset.editorFontFamily = TEST_EDITOR_FONT_FAMILY;
+ el.dataset.signInPath = TEST_SIGN_IN_PATH;
document.body.append(el);
};
@@ -109,6 +111,7 @@ describe('ide/init_gitlab_web_ide', () => {
links: {
userPreferences: TEST_USER_PREFERENCES_PATH,
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
+ signIn: TEST_SIGN_IN_PATH,
},
editorFont: {
srcUrl: TEST_EDITOR_FONT_SRC_URL,
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index b158cfff10d..bce335aa035 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -375,6 +375,17 @@ describe('issue_note', () => {
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
});
+ it('should not update note with sensitive token', () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ createWrapper();
+ updateActions();
+ wrapper
+ .findComponent(NoteBody)
+ .vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage });
+ expect(updateNote).not.toHaveBeenCalled();
+ });
+
it('does not stringify empty position', () => {
createWrapper();
updateActions();
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 0bfca9a290b..3dae1a08033 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -333,6 +333,7 @@ RSpec.describe GitlabSchema.types['Project'] do
:target_branches,
:state,
:draft,
+ :approved,
:labels,
:before,
:after,
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index e5a39f6a24e..4a184f03935 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
'can-use-new-web-ide' => 'false',
'use-new-web-ide' => 'false',
'user-preferences-path' => profile_preferences_path,
+ 'sign-in-path' => 'test-sign-in-path',
'project' => nil,
'preview-markdown-path' => nil
}
@@ -29,6 +30,7 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:content_security_policy_nonce).and_return('test-csp-nonce')
+ allow(helper).to receive(:new_session_path).and_return('test-sign-in-path')
end
it 'returns hash' do
@@ -99,6 +101,7 @@ RSpec.describe IdeHelper, feature_category: :web_ide do
'can-use-new-web-ide' => 'true',
'use-new-web-ide' => 'true',
'user-preferences-path' => profile_preferences_path,
+ 'sign-in-path' => 'test-sign-in-path',
'new-web-ide-help-page-path' =>
help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
'csp-nonce' => 'test-csp-nonce',
diff --git a/spec/lib/bulk_imports/clients/graphql_spec.rb b/spec/lib/bulk_imports/clients/graphql_spec.rb
index 58e6992698c..5ded54a3448 100644
--- a/spec/lib/bulk_imports/clients/graphql_spec.rb
+++ b/spec/lib/bulk_imports/clients/graphql_spec.rb
@@ -13,10 +13,6 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
let(:response_double) { double }
let(:version) { '14.0.0' }
- before do
- stub_const('BulkImports::MINIMUM_COMPATIBLE_MAJOR_VERSION', version)
- end
-
describe 'source instance validation' do
before do
allow(graphql_client_double).to receive(:execute)
@@ -37,7 +33,7 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
let(:version) { '13.0.0' }
it 'raises an error' do
- expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab version. Source instance must run GitLab version #{BulkImport::MIN_MAJOR_VERSION} or later.")
+ expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab version. Minimum supported version is 14.")
end
end
end
diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb
index 23dfe865ba3..70c581716ce 100644
--- a/spec/requests/api/bulk_imports_spec.rb
+++ b/spec/requests/api/bulk_imports_spec.rb
@@ -93,6 +93,9 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}
end
+ let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[:entities][0][:source_type]) }
+ let(:source_entity_identifier) { ERB::Util.url_encode(params[:entities][0][:source_full_path]) }
+
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
allow(instance)
@@ -103,6 +106,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
.to receive(:instance_enterprise)
.and_return(false)
end
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
+ .to_return(status: 200, body: "", headers: {})
end
shared_examples 'starting a new migration' do
@@ -271,12 +276,41 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}
end
+ it 'returns blocked url message in the error' do
+ request
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+
+ expect(json_response['message']).to include("Url is blocked: Only allowed schemes are http, https")
+ end
+ end
+
+ context 'when source instance setting is disabled' do
+ let(:params) do
+ {
+ configuration: {
+ url: 'http://gitlab.example',
+ access_token: 'access_token'
+ },
+ entities: [
+ source_type: 'group_entity',
+ source_full_path: 'full_path',
+ destination_slug: 'destination_slug',
+ destination_namespace: 'destination_namespace'
+ ]
+ }
+ end
+
it 'returns blocked url error' do
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
+ .to_return(status: 404, body: "", headers: {})
+
request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response['message']).to eq('Validation failed: Url is blocked: Only allowed schemes are http, https')
+ expect(json_response['message']).to include("Group import disabled on source or destination instance. " \
+ "Ask an administrator to enable it on both instances and try again.")
end
end
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 156886ca211..9c24dbbfb76 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -226,6 +226,28 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
it_behaves_like 'when searching with parameters'
end
+ context 'when searching by approved' do
+ let(:approved_mr) { create(:merge_request, target_project: project, source_project: project) }
+
+ before do
+ create(:approval, merge_request: approved_mr)
+ end
+
+ context 'when true' do
+ let(:search_params) { { approved: true } }
+ let(:mrs) { [approved_mr] }
+
+ it_behaves_like 'when searching with parameters'
+ end
+
+ context 'when false' do
+ let(:search_params) { { approved: false } }
+ let(:mrs) { all_merge_requests }
+
+ it_behaves_like 'when searching with parameters'
+ end
+ end
+
context 'when requesting `approved_by`' do
let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } }
let(:extra_iid_for_second_query) { merge_request_c.iid.to_s }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 0dca21f752f..31a14d30cb7 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -249,6 +249,35 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
expect(response).to match_response_schema('public_api/v4/merge_requests')
end
+ context 'with approved param' do
+ let(:approved_mr) { create(:merge_request, target_project: project, source_project: project) }
+
+ before do
+ create(:approval, merge_request: approved_mr)
+ end
+
+ it 'returns only approved merge requests' do
+ path = endpoint_path + '?approved=yes'
+
+ get api(path, user)
+
+ expect_paginated_array_response([approved_mr.id])
+ end
+
+ it 'returns only non-approved merge requests' do
+ path = endpoint_path + '?approved=no'
+
+ get api(path, user)
+
+ expect_paginated_array_response([
+ merge_request_merged.id,
+ merge_request_locked.id,
+ merge_request_closed.id,
+ merge_request.id
+ ])
+ end
+ end
+
it 'returns an empty array if no issue matches milestone' do
get api(endpoint_path, user), params: { milestone: '1.0.0' }
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 2f67e1e8eea..c738269fce0 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
+ include ExclusiveLeaseHelpers
+
include_context 'npm api setup'
shared_examples 'accept get request on private project with access to package registry for everyone' do
@@ -368,6 +370,25 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
end
+ context 'when the lease to create a package is already taken' do
+ let(:params) { upload_params(package_name: package_name) }
+ let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}" }
+
+ before do
+ stub_exclusive_lease_taken(lease_key, timeout: Packages::Npm::CreatePackageService::DEFAULT_LEASE_TIMEOUT)
+ end
+
+ it_behaves_like 'not a package tracking event'
+
+ it 'returns an error' do
+ expect { upload_package_with_token }
+ .not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include('Could not obtain package lease.')
+ end
+ end
+
context 'with a too large metadata structure' do
let(:package_name) { "@#{group.path}/my_package_name" }
let(:params) do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 0f70d844d9f..79ad8926b16 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -55,8 +55,8 @@ RSpec.describe API::Projects, feature_category: :projects do
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:admin) { create(:admin) }
- let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace) }
- let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) }
+ let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace, updated_at: 5.days.ago) }
+ let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace, updated_at: 4.days.ago) }
let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let_it_be(:user4) { create(:user, username: 'user.withdot') }
let_it_be(:project3, reload: true) do
@@ -510,6 +510,35 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
+ context 'filter by updated_at' do
+ let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: :updated_at } }
+
+ it_behaves_like 'projects response' do
+ let(:current_user) { user }
+ let(:projects) { [project2, project] }
+ end
+
+ it 'returns projects sorted by updated_at' do
+ get api('/projects', user), params: filter
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |p| p['id'] }).to match([project2, project].map(&:id))
+ end
+
+ context 'when filtering by updated_at and sorting by a different column' do
+ let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: 'id' } }
+
+ it 'returns an error' do
+ get api('/projects', user), params: filter
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(
+ '400 Bad request - `updated_at` filter and `updated_at` sorting must be paired'
+ )
+ end
+ end
+ end
+
context 'and using search' do
it_behaves_like 'projects response' do
let(:filter) { { search: project.name } }
@@ -1643,6 +1672,16 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.first.keys).to include('container_registry_access_level')
end
+ context 'filter by updated_at' do
+ it 'returns only projects updated on the given timeframe' do
+ get api("/users/#{user.id}/projects", user),
+ params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id)
+ end
+ end
+
context 'and using id_after' do
let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
@@ -1774,6 +1813,16 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.map { |project| project['id'] })
.to contain_exactly(project.id, project2.id, project3.id)
end
+
+ context 'filter by updated_at' do
+ it 'returns only projects updated on the given timeframe' do
+ get api("/users/#{user3.id}/starred_projects/", user),
+ params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id)
+ end
+ end
end
context 'with a private profile' do
@@ -3073,9 +3122,9 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'fork management' do
- let(:project_fork_target) { create(:project) }
- let(:project_fork_source) { create(:project, :public) }
- let(:private_project_fork_source) { create(:project, :private) }
+ let_it_be_with_refind(:project_fork_target) { create(:project) }
+ let_it_be_with_refind(:project_fork_source) { create(:project, :public) }
+ let_it_be_with_refind(:private_project_fork_source) { create(:project, :private) }
describe 'POST /projects/:id/fork/:forked_from_id' do
context 'user is a developer' do
@@ -3221,11 +3270,11 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /projects/:id/forks' do
- let(:private_fork) { create(:project, :private, :empty_repo) }
- let(:member) { create(:user) }
- let(:non_member) { create(:user) }
+ let_it_be_with_refind(:private_fork) { create(:project, :private, :empty_repo) }
+ let_it_be(:member) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
- before do
+ before_all do
private_fork.add_developer(member)
end
@@ -3249,6 +3298,20 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.length).to eq(1)
expect(json_response[0]['name']).to eq(private_fork.name)
end
+
+ context 'filter by updated_at' do
+ before do
+ private_fork.update!(updated_at: 4.days.ago)
+ end
+
+ it 'returns only forks updated on the given timeframe' do
+ get api("/projects/#{project_fork_source.id}/forks", member),
+ params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(private_fork.id)
+ end
+ end
end
context 'for a user that cannot access the forks' do
diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb
index 7f892cfe722..35398e4cae0 100644
--- a/spec/services/bulk_imports/create_service_spec.rb
+++ b/spec/services/bulk_imports/create_service_spec.rb
@@ -35,6 +35,9 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
]
end
+ let(:source_entity_identifier) { ERB::Util.url_encode(params[0][:source_full_path]) }
+ let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[0][:source_type]) }
+
subject { described_class.new(user, params, credentials) }
describe '#execute' do
@@ -59,6 +62,34 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
end
end
+ context 'when direct transfer setting query returns a 404' do
+ it 'raises a ServiceResponse::Error' do
+ stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
+ stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token')
+ .to_return(
+ status: 200,
+ body: source_version.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
+ .to_return(status: 404)
+
+ expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ expect(client).to receive(:get).and_raise(BulkImports::Error.setting_not_enabled)
+ end
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message)
+ .to eq(
+ "Group import disabled on source or destination instance. " \
+ "Ask an administrator to enable it on both instances and try again."
+ )
+ end
+ end
+
context 'when required scopes are not present' do
it 'returns ServiceResponse with error if token does not have api scope' do
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
@@ -68,9 +99,13 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
body: source_version.to_json,
headers: { 'Content-Type' => 'application/json' }
)
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
+ .to_return(
+ status: 200
+ )
allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
- allow(client).to receive(:validate_instance_version!).and_raise(BulkImports::Error.scope_validation_failure)
+ allow(client).to receive(:validate_import_scopes!).and_raise(BulkImports::Error.scope_validation_failure)
end
result = subject.execute
@@ -90,6 +125,10 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token')
.to_return(status: 200, body: source_version.to_json, headers: { 'Content-Type' => 'application/json' })
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
+ .to_return(
+ status: 200
+ )
stub_request(:get, 'http://gitlab.example/api/v4/personal_access_tokens/self?private_token=token')
.to_return(
status: 200,
@@ -169,6 +208,10 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
allow(instance).to receive(:instance_version).and_return(source_version)
allow(instance).to receive(:instance_enterprise).and_return(false)
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
+ .to_return(
+ status: 200
+ )
end
end
@@ -325,6 +368,105 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
end
end
+ describe '.validate_setting_enabled!' do
+ let(:entity_source_id) { 'gid://gitlab/Model/12345' }
+ let(:graphql_client) { instance_double(BulkImports::Clients::Graphql) }
+ let(:http_client) { instance_double(BulkImports::Clients::HTTP) }
+ let(:http_response) { double(code: 200, success?: true) } # rubocop:disable RSpec/VerifiedDoubles
+
+ before do
+ allow(BulkImports::Clients::HTTP).to receive(:new).and_return(http_client)
+ allow(BulkImports::Clients::Graphql).to receive(:new).and_return(graphql_client)
+
+ allow(http_client).to receive(:instance_version).and_return(status: 200)
+ allow(http_client).to receive(:instance_enterprise).and_return(false)
+ allow(http_client).to receive(:validate_instance_version!).and_return(source_version)
+ allow(http_client).to receive(:validate_import_scopes!).and_return(true)
+ end
+
+ context 'when the source_type is a group' do
+ context 'when the source_full_path contains only integer characters' do
+ let(:query_string) { BulkImports::Groups::Graphql::GetGroupQuery.new(context: nil).to_s }
+ let(:graphql_response) do
+ double(original_hash: { 'data' => { 'group' => { 'id' => entity_source_id } } }) # rubocop:disable RSpec/VerifiedDoubles
+ end
+
+ let(:params) do
+ [
+ {
+ source_type: 'group_entity',
+ source_full_path: '67890',
+ destination_slug: 'destination-group-1',
+ destination_namespace: 'destination1'
+ }
+ ]
+ end
+
+ before do
+ allow(graphql_client).to receive(:parse).with(query_string)
+ allow(graphql_client).to receive(:execute).and_return(graphql_response)
+
+ allow(http_client).to receive(:get)
+ .with("/groups/12345/export_relations/status")
+ .and_return(http_response)
+
+ stub_request(:get, "http://gitlab.example/api/v4/groups/12345/export_relations/status?page=1&per_page=30&private_token=token")
+ .to_return(status: 200, body: "", headers: {})
+ end
+
+ it 'makes a graphql request using the group full path and an http request with the correct id' do
+ expect(graphql_client).to receive(:parse).with(query_string)
+ expect(graphql_client).to receive(:execute).and_return(graphql_response)
+
+ expect(http_client).to receive(:get).with("/groups/12345/export_relations/status")
+
+ subject.execute
+ end
+ end
+ end
+
+ context 'when the source_type is a project' do
+ context 'when the source_full_path contains only integer characters' do
+ let(:query_string) { BulkImports::Projects::Graphql::GetProjectQuery.new(context: nil).to_s }
+ let(:graphql_response) do
+ double(original_hash: { 'data' => { 'project' => { 'id' => entity_source_id } } }) # rubocop:disable RSpec/VerifiedDoubles
+ end
+
+ let(:params) do
+ [
+ {
+ source_type: 'project_entity',
+ source_full_path: '67890',
+ destination_slug: 'destination-group-1',
+ destination_namespace: 'destination1'
+ }
+ ]
+ end
+
+ before do
+ allow(graphql_client).to receive(:parse).with(query_string)
+ allow(graphql_client).to receive(:execute).and_return(graphql_response)
+
+ allow(http_client).to receive(:get)
+ .with("/projects/12345/export_relations/status")
+ .and_return(http_response)
+
+ stub_request(:get, "http://gitlab.example/api/v4/projects/12345/export_relations/status?page=1&per_page=30&private_token=token")
+ .to_return(status: 200, body: "", headers: {})
+ end
+
+ it 'makes a graphql request using the group full path and an http request with the correct id' do
+ expect(graphql_client).to receive(:parse).with(query_string)
+ expect(graphql_client).to receive(:execute).and_return(graphql_response)
+
+ expect(http_client).to receive(:get).with("/projects/12345/export_relations/status")
+
+ subject.execute
+ end
+ end
+ end
+ end
+
describe '.validate_destination_full_path' do
context 'when the source_type is a group' do
context 'when the provided destination_slug already exists in the destination_namespace' do
diff --git a/spec/services/issuable/callbacks/milestone_spec.rb b/spec/services/issuable/callbacks/milestone_spec.rb
new file mode 100644
index 00000000000..0c0d7d24d20
--- /dev/null
+++ b/spec/services/issuable/callbacks/milestone_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issuable::Callbacks::Milestone, feature_category: :team_planning do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:project_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:reporter) do
+ create(:user).tap { |u| project.add_reporter(u) }
+ end
+
+ let(:issuable) { build(:issue, project: project) }
+ let(:current_user) { reporter }
+ let(:params) { { milestone_id: project_milestone.id } }
+ let(:callback) { described_class.new(issuable: issuable, current_user: current_user, params: params) }
+
+ describe '#after_initialize' do
+ it "sets the issuable's milestone" do
+ expect { callback.after_initialize }.to change { issuable.milestone }.from(nil).to(project_milestone)
+ end
+
+ context 'when assigning a group milestone' do
+ let(:params) { { milestone_id: group_milestone.id } }
+
+ it "sets the issuable's milestone" do
+ expect { callback.after_initialize }.to change { issuable.milestone }.from(nil).to(group_milestone)
+ end
+ end
+
+ context 'when assigning a group milestone outside the project ancestors' do
+ let(:another_group_milestone) { create(:milestone, group: create(:group)) }
+ let(:params) { { milestone_id: another_group_milestone.id } }
+
+ it "does not change the issuable's milestone" do
+ expect { callback.after_initialize }.not_to change { issuable.milestone }
+ end
+ end
+
+ context 'when user is not allowed to set issuable metadata' do
+ let(:current_user) { create(:user) }
+
+ it "does not change the issuable's milestone" do
+ expect { callback.after_initialize }.not_to change { issuable.milestone }
+ end
+ end
+
+ context 'when unsetting a milestone' do
+ let(:issuable) { create(:issue, project: project, milestone: project_milestone) }
+
+ context 'when milestone_id is nil' do
+ let(:params) { { milestone_id: nil } }
+
+ it "unsets the issuable's milestone" do
+ expect { callback.after_initialize }.to change { issuable.milestone }.from(project_milestone).to(nil)
+ end
+ end
+
+ context 'when milestone_id is an empty string' do
+ let(:params) { { milestone_id: '' } }
+
+ it "unsets the issuable's milestone" do
+ expect { callback.after_initialize }.to change { issuable.milestone }.from(project_milestone).to(nil)
+ end
+ end
+
+ context 'when milestone_id is 0' do
+ let(:params) { { milestone_id: '0' } }
+
+ it "unsets the issuable's milestone" do
+ expect { callback.after_initialize }.to change { issuable.milestone }.from(project_milestone).to(nil)
+ end
+ end
+
+ context 'when milestone_id is not given' do
+ let(:params) { {} }
+
+ it "does not unset the issuable's milestone" do
+ expect { callback.after_initialize }.not_to change { issuable.milestone }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/after_create_service_spec.rb b/spec/services/issues/after_create_service_spec.rb
index 594caed23d7..b59578b14a0 100644
--- a/spec/services/issues/after_create_service_spec.rb
+++ b/spec/services/issues/after_create_service_spec.rb
@@ -28,13 +28,6 @@ RSpec.describe Issues::AfterCreateService, feature_category: :team_planning do
expect { after_create_service.execute(issue) }.to change { Todo.where(attributes).count }.by(1)
end
- it 'deletes milestone issues count cache' do
- expect_next(Milestones::IssuesCountService, milestone)
- .to receive(:delete_cache).and_call_original
-
- after_create_service.execute(issue)
- end
-
context 'with a regular issue' do
it_behaves_like 'does not track incident management event', :incident_management_incident_created do
subject { after_create_service.execute(issue) }
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 0f89a746520..fecfc3f3d64 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -161,8 +161,8 @@ RSpec.describe Issues::BuildService, feature_category: :team_planning do
end
end
- context 'when guest' do
- let(:user) { guest }
+ context 'when user is not a project member' do
+ let(:user) { create(:user) }
it 'cannot set milestone' do
milestone = create(:milestone, project: project)
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 94dfdc618de..df47780bc89 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -124,6 +124,15 @@ RSpec.describe Issues::CreateService, feature_category: :team_planning do
expect(issue.issue_customer_relations_contacts).to be_empty
end
+ context 'with milestone' do
+ it 'deletes milestone issues count cache' do
+ expect_next(Milestones::IssuesCountService, milestone)
+ .to receive(:delete_cache).and_call_original
+
+ expect(result).to be_success
+ end
+ end
+
context 'when the work item type is not allowed to create' do
before do
allow_next_instance_of(::Issues::BuildService) do |instance|
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index f2823b1f0c7..9361ec44e30 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -143,22 +143,6 @@ RSpec.describe MergeRequests::AfterCreateService, feature_category: :code_review
expect { execute_service }.to change { counter.read(:create) }.by(1)
end
- context 'with a milestone' do
- let(:milestone) { create(:milestone, project: merge_request.target_project) }
-
- before do
- merge_request.update!(milestone_id: milestone.id)
- end
-
- it 'deletes the cache key for milestone merge request counter', :use_clean_rails_memory_store_caching do
- expect_next_instance_of(Milestones::MergeRequestsCountService, milestone) do |service|
- expect(service).to receive(:delete_cache).and_call_original
- end
-
- execute_service
- end
- end
-
context 'todos' do
it 'does not creates todos' do
attributes = {
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 7e20af32985..af77abf252c 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state, feature_category: :code_review_workflow do
include ProjectForksHelper
+ include AfterNextHelpers
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -336,6 +337,19 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state, f
end
end
+ context 'with a milestone' do
+ let(:milestone) { create(:milestone, project: project) }
+
+ let(:opts) { { title: 'Awesome merge_request', source_branch: 'feature', target_branch: 'master', milestone_id: milestone.id } }
+
+ it 'deletes the cache key for milestone merge request counter' do
+ expect_next(Milestones::MergeRequestsCountService, milestone)
+ .to receive(:delete_cache).and_call_original
+
+ expect(merge_request).to be_persisted
+ end
+ end
+
it_behaves_like 'reviewer_ids filter' do
let(:execute) { service.execute }
end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index 70c79dae437..9ecb1130879 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_registry do
+ include ExclusiveLeaseHelpers
+
let(:namespace) { create(:namespace) }
let(:project) { create(:project, namespace: namespace) }
let(:user) { create(:user) }
@@ -15,8 +17,10 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
let(:package_name) { "@#{namespace.path}/my-app" }
let(:version_data) { params.dig('versions', '1.0.1') }
+ let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}" }
+ let(:service) { described_class.new(project, user, params) }
- subject { described_class.new(project, user, params).execute }
+ subject { service.execute }
shared_examples 'valid package' do
it 'creates a package' do
@@ -216,5 +220,65 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
it { expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Version is invalid') }
end
end
+
+ it 'obtains a lease to create a new package' do
+ expect_to_obtain_exclusive_lease(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
+
+ subject
+ end
+
+ context 'with npm_obtain_lease_to_create_package disabled' do
+ before do
+ stub_feature_flags(npm_obtain_lease_to_create_package: false)
+ end
+
+ it 'does not obtain a lease' do
+ lease = stub_exclusive_lease(lease_key, 'uuid', timeout: described_class::DEFAULT_LEASE_TIMEOUT)
+
+ expect(lease).not_to receive(:try_obtain)
+
+ subject
+ end
+ end
+
+ context 'when the lease is already taken' do
+ before do
+ stub_exclusive_lease_taken(lease_key, timeout: described_class::DEFAULT_LEASE_TIMEOUT)
+ end
+
+ it { expect(subject[:http_status]).to eq 400 }
+ it { expect(subject[:message]).to eq 'Could not obtain package lease.' }
+ end
+
+ context 'when many of the same packages are created at the same time', :delete do
+ it 'only creates one package' do
+ expect { package_creation_race(project, user, params) }.to change { Packages::Package.count }.by(1)
+ end
+ end
+
+ def package_creation_race(project, user, params)
+ # create a race condition - structure from https://blog.arkency.com/2015/09/testing-race-conditions/
+ wait_for_it = true
+
+ threads = Array.new(5) do |_|
+ Thread.new do
+ # A loop to make threads busy until we `join` them
+ true while wait_for_it
+
+ described_class.new(project, user, params).execute
+ end
+ end
+
+ wait_for_it = false
+ threads.each(&:join)
+ end
+ end
+
+ describe '#lease_key' do
+ subject { service.send(:lease_key) }
+
+ it 'returns an unique key' do
+ is_expected.to eq lease_key
+ end
end
end
diff --git a/spec/services/tasks_to_be_done/base_service_spec.rb b/spec/services/tasks_to_be_done/base_service_spec.rb
index ff4eefdfb3a..3ca9d140197 100644
--- a/spec/services/tasks_to_be_done/base_service_spec.rb
+++ b/spec/services/tasks_to_be_done/base_service_spec.rb
@@ -33,9 +33,9 @@ RSpec.describe TasksToBeDone::BaseService, feature_category: :team_planning do
add_labels: label.title
}
- expect(Issues::BuildService)
+ expect(Issues::CreateService)
.to receive(:new)
- .with(container: project, current_user: current_user, params: params)
+ .with(container: project, current_user: current_user, params: params, spam_params: nil)
.and_call_original
expect { service.execute }.to change(Issue, :count).by(1)
diff --git a/spec/services/work_items/widgets/milestone_service/create_service_spec.rb b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
deleted file mode 100644
index 64ab2421c74..00000000000
--- a/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe WorkItems::Widgets::MilestoneService::CreateService, feature_category: :portfolio_management do
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, :private, group: group) }
- let_it_be(:project_milestone) { create(:milestone, project: project) }
- let_it_be(:group_milestone) { create(:milestone, group: group) }
- let_it_be(:guest) { create(:user) }
-
- let(:current_user) { guest }
- let(:work_item) { build(:work_item, project: project, updated_at: 1.day.ago) }
- let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
- let(:service) { described_class.new(widget: widget, current_user: current_user) }
-
- before do
- project.add_guest(guest)
- end
-
- describe '#before_create_callback' do
- it_behaves_like "setting work item's milestone" do
- subject(:execute_callback) do
- service.before_create_callback(params: params)
- end
- end
- end
-end
diff --git a/spec/services/work_items/widgets/milestone_service/update_service_spec.rb b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
deleted file mode 100644
index c5bc2b12fc5..00000000000
--- a/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe WorkItems::Widgets::MilestoneService::UpdateService, feature_category: :portfolio_management do
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, :private, group: group) }
- let_it_be(:project_milestone) { create(:milestone, project: project) }
- let_it_be(:group_milestone) { create(:milestone, group: group) }
- let_it_be(:reporter) { create(:user) }
- let_it_be(:guest) { create(:user) }
-
- let(:work_item) { create(:work_item, project: project, updated_at: 1.day.ago) }
- let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
- let(:service) { described_class.new(widget: widget, current_user: current_user) }
-
- before do
- project.add_reporter(reporter)
- project.add_guest(guest)
- end
-
- describe '#before_update_callback' do
- context 'when current user is not allowed to set work item metadata' do
- let(:current_user) { guest }
- let(:params) { { milestone_id: group_milestone.id } }
-
- it "does not set the work item's milestone" do
- expect { service.before_update_callback(params: params) }
- .to not_change(work_item, :milestone)
- end
- end
-
- context "when current user is allowed to set work item metadata" do
- let(:current_user) { reporter }
-
- it_behaves_like "setting work item's milestone" do
- subject(:execute_callback) do
- service.before_update_callback(params: params)
- end
- end
-
- context 'when unsetting a milestone' do
- let(:params) { { milestone_id: nil } }
-
- before do
- work_item.update!(milestone: project_milestone)
- end
-
- it "sets the work item's milestone" do
- expect { service.before_update_callback(params: params) }
- .to change(work_item, :milestone)
- .from(project_milestone)
- .to(nil)
- end
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
deleted file mode 100644
index ac064ed4c33..00000000000
--- a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples "setting work item's milestone" do
- context "when 'milestone' param does not exist" do
- let(:params) { {} }
-
- it "does not set the work item's milestone" do
- expect { execute_callback }.to not_change(work_item, :milestone)
- end
- end
-
- context "when 'milestone' is not in the work item's project's hierarchy" do
- let(:another_group_milestone) { create(:milestone, group: create(:group)) }
- let(:params) { { milestone_id: another_group_milestone.id } }
-
- it "does not set the work item's milestone" do
- expect { execute_callback }.to not_change(work_item, :milestone)
- end
- end
-
- context 'when assigning a group milestone' do
- let(:params) { { milestone_id: group_milestone.id } }
-
- it "sets the work item's milestone" do
- expect { execute_callback }
- .to change { work_item.milestone }
- .from(nil)
- .to(group_milestone)
- end
- end
-
- context 'when assigning a project milestone' do
- let(:params) { { milestone_id: project_milestone.id } }
-
- it "sets the work item's milestone" do
- expect { execute_callback }
- .to change { work_item.milestone }
- .from(nil)
- .to(project_milestone)
- end
- end
-end
diff --git a/spec/tooling/lib/tooling/find_tests_spec.rb b/spec/tooling/lib/tooling/find_tests_spec.rb
index 997d9d9a746..e531fb9548e 100644
--- a/spec/tooling/lib/tooling/find_tests_spec.rb
+++ b/spec/tooling/lib/tooling/find_tests_spec.rb
@@ -50,15 +50,17 @@ RSpec.describe Tooling::FindTests, feature_category: :tooling do
subject { instance.execute }
context 'when the matching_tests_paths file does not exist' do
- before do
- allow(File).to receive(:exist?).and_return(false)
- allow(File).to receive(:write).with(matching_tests_paths, any_args)
- end
+ let(:instance) { described_class.new(non_existing_output_file, matching_tests_paths) }
+ let(:non_existing_output_file) { 'tmp/another_file.out' }
- it 'creates an empty file' do
- expect(File).to receive(:write).with(matching_tests_paths, '')
+ around do |example|
+ example.run
+ ensure
+ FileUtils.rm_rf(non_existing_output_file)
+ end
- subject
+ it 'creates the file' do
+ expect { subject }.to change { File.exist?(non_existing_output_file) }.from(false).to(true)
end
end
diff --git a/spec/tooling/lib/tooling/helpers/file_handler_spec.rb b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb
new file mode 100644
index 00000000000..7c8310c4bd9
--- /dev/null
+++ b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'tempfile'
+require_relative '../../../../../tooling/lib/tooling/helpers/file_handler'
+
+class MockClass # rubocop:disable Gitlab/NamespacedClass
+ include Tooling::Helpers::FileHandler
+end
+
+RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do
+ attr_accessor :input_file_path, :output_file_path
+
+ around do |example|
+ input_file = Tempfile.new('input')
+ output_file = Tempfile.new('output')
+
+ self.input_file_path = input_file.path
+ self.output_file_path = output_file.path
+
+ # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
+ # Tempfile.html#class-Tempfile-label-Explicit+close
+ begin
+ example.run
+ ensure
+ output_file.close
+ input_file.close
+ output_file.unlink
+ input_file.unlink
+ end
+ end
+
+ let(:instance) { MockClass.new }
+ let(:initial_content) { 'previous_content1 previous_content2' }
+
+ before do
+ # We write into the temp files initially, to later check how the code modified those files
+ File.write(input_file_path, initial_content)
+ File.write(output_file_path, initial_content)
+ end
+
+ describe '#read_array_from_file' do
+ subject { instance.read_array_from_file(input_file_path) }
+
+ context 'when the input file does not exist' do
+ let(:non_existing_input_file) { 'tmp/another_file.out' }
+
+ subject { instance.read_array_from_file(non_existing_input_file) }
+
+ around do |example|
+ example.run
+ ensure
+ FileUtils.rm_rf(non_existing_input_file)
+ end
+
+ it 'creates the file' do
+ expect { subject }.to change { File.exist?(non_existing_input_file) }.from(false).to(true)
+ end
+ end
+
+ context 'when the input file is not empty' do
+ let(:initial_content) { 'previous_content1 previous_content2' }
+
+ it 'returns the content of the file in an array' do
+ expect(subject).to eq(initial_content.split(' '))
+ end
+ end
+ end
+
+ describe '#write_array_to_file' do
+ let(:content_array) { %w[new_entry] }
+
+ subject { instance.write_array_to_file(output_file_path, content_array) }
+
+ context 'when the output file does not exist' do
+ let(:non_existing_output_file) { 'tmp/another_file.out' }
+
+ subject { instance.write_array_to_file(non_existing_output_file, content_array) }
+
+ around do |example|
+ example.run
+ ensure
+ FileUtils.rm_rf(non_existing_output_file)
+ end
+
+ it 'creates the file' do
+ expect { subject }.to change { File.exist?(non_existing_output_file) }.from(false).to(true)
+ end
+ end
+
+ context 'when the output file is empty' do
+ let(:initial_content) { '' }
+
+ it 'writes the correct content to the file' do
+ expect { subject }.to change { File.read(output_file_path) }.from('').to(content_array.join(' '))
+ end
+ end
+
+ context 'when the output file is not empty' do
+ let(:initial_content) { 'previous_content1 previous_content2' }
+
+ it 'appends the correct content to the file' do
+ expect { subject }.to change { File.read(output_file_path) }
+ .from(initial_content)
+ .to((initial_content.split(' ') + content_array).join(' '))
+ end
+ end
+ end
+end
diff --git a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
index 375be001bd1..30965ed985d 100644
--- a/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
+++ b/spec/tooling/lib/tooling/mappings/partial_to_views_mappings_spec.rb
@@ -52,8 +52,8 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
end
context 'when no partials were modified' do
- it 'empties the output file' do
- expect { subject }.to change { File.read(output_file) }.from(output_file_content).to('')
+ it 'does not change the output file' do
+ expect { subject }.not_to change { File.read(output_file) }
end
end
@@ -76,8 +76,8 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
File.write("#{view_base_folder}/my_view.html.haml", "render 'another_partial'")
end
- it 'empties the output file' do
- expect { subject }.to change { File.read(output_file) }.from(output_file_content).to('')
+ it 'does not change the output file' do
+ expect { subject }.not_to change { File.read(output_file) }
end
end
@@ -89,7 +89,7 @@ RSpec.describe Tooling::Mappings::PartialToViewsMappings, feature_category: :too
it 'writes the view including the partial to the output' do
expect { subject }.to change { File.read(output_file) }
.from(output_file_content)
- .to("#{view_base_folder}/my_view.html.haml")
+ .to(output_file_content + " #{view_base_folder}/my_view.html.haml")
end
end
end
diff --git a/tooling/lib/tooling/find_tests.rb b/tooling/lib/tooling/find_tests.rb
index 4acbb5fcd6a..b63b207c58b 100644
--- a/tooling/lib/tooling/find_tests.rb
+++ b/tooling/lib/tooling/find_tests.rb
@@ -1,16 +1,15 @@
# frozen_string_literal: true
require 'test_file_finder'
+require_relative 'helpers/file_handler'
module Tooling
class FindTests
+ include Helpers::FileHandler
+
def initialize(changes_file, matching_tests_paths)
@matching_tests_paths = matching_tests_paths
- @changed_files = File.read(changes_file).split(' ')
-
- File.write(matching_tests_paths, '') unless File.exist?(matching_tests_paths)
-
- @matching_tests = File.read(matching_tests_paths).split(' ')
+ @changed_files = read_array_from_file(changes_file)
end
def execute
@@ -22,8 +21,7 @@ module Tooling
end
end
- new_matching_tests = tff.test_files.uniq
- File.write(matching_tests_paths, (matching_tests + new_matching_tests).join(' '))
+ write_array_to_file(matching_tests_paths, tff.test_files.uniq)
end
private
diff --git a/tooling/lib/tooling/helpers/file_handler.rb b/tooling/lib/tooling/helpers/file_handler.rb
new file mode 100644
index 00000000000..ec4f42ea363
--- /dev/null
+++ b/tooling/lib/tooling/helpers/file_handler.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+
+module Tooling
+ module Helpers
+ module FileHandler
+ def read_array_from_file(file)
+ FileUtils.touch file
+
+ File.read(file).split(' ')
+ end
+
+ def write_array_to_file(file, content_array)
+ FileUtils.touch file
+
+ output_content = (File.read(file).split(' ') + content_array).join(' ')
+
+ File.write(file, output_content)
+ end
+ end
+ end
+end
diff --git a/tooling/lib/tooling/mappings/base.rb b/tooling/lib/tooling/mappings/base.rb
index 93d3a967114..3db4d1dbb9d 100644
--- a/tooling/lib/tooling/mappings/base.rb
+++ b/tooling/lib/tooling/mappings/base.rb
@@ -1,11 +1,14 @@
# frozen_string_literal: true
require_relative '../../../../lib/gitlab_edition'
+require_relative '../helpers/file_handler'
# Returns system specs files that are related to the JS files that were changed in the MR.
module Tooling
module Mappings
class Base
+ include Helpers::FileHandler
+
# Input: A list of space-separated files
# Output: A list of space-separated specs files (JS, Ruby, ...)
def execute(changed_files)
diff --git a/tooling/lib/tooling/mappings/partial_to_views_mappings.rb b/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
index c0cf378b53b..3109da685f1 100644
--- a/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
+++ b/tooling/lib/tooling/mappings/partial_to_views_mappings.rb
@@ -9,7 +9,7 @@ module Tooling
class PartialToViewsMappings < Base
def initialize(changes_file, output_file, view_base_folder: 'app/views')
@output_file = output_file
- @changed_files = File.read(changes_file).split(' ')
+ @changed_files = read_array_from_file(changes_file)
@view_base_folders = folders_for_available_editions(view_base_folder)
end
@@ -28,7 +28,7 @@ module Tooling
end
end
- File.write(output_file, views_including_modified_partials.join(' '))
+ write_array_to_file(output_file, views_including_modified_partials)
end
def filter_files