summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-12-08 12:13:04 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-12-08 12:13:04 +0000
commit886ecba0bd2d964504b43303a39cfa2386f0feed (patch)
treee814b9f24f3df16bc1a8c8725a168fac3844d719
parentcb09086128f2923126d009a88b478ff3919c8309 (diff)
downloadgitlab-ce-886ecba0bd2d964504b43303a39cfa2386f0feed.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.ruby-version2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/flash.js7
-rw-r--r--app/assets/javascripts/issuable/issuable_template_selector.js (renamed from app/assets/javascripts/templates/issuable_template_selector.js)6
-rw-r--r--app/assets/javascripts/issuable/issuable_template_selectors.js (renamed from app/assets/javascripts/templates/issuable_template_selectors.js)5
-rw-r--r--app/assets/javascripts/issues/filtered_search_service_desk.js (renamed from app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js)0
-rw-r--r--app/assets/javascripts/issues/form.js (renamed from app/assets/javascripts/pages/projects/issues/form.js)2
-rw-r--r--app/assets/javascripts/issues/init_filtered_search_service_desk.js11
-rw-r--r--app/assets/javascripts/issues/issue.js (renamed from app/assets/javascripts/issue.js)12
-rw-r--r--app/assets/javascripts/issues/manual_ordering.js (renamed from app/assets/javascripts/manual_ordering.js)0
-rw-r--r--app/assets/javascripts/issues/show.js (renamed from app/assets/javascripts/pages/projects/issues/show.js)2
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description_template.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue2
-rw-r--r--app/assets/javascripts/main.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue13
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql4
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js58
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue14
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js14
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue9
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue3
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue71
-rw-r--r--app/assets/javascripts/work_items/graphql/resolvers.js25
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql10
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql18
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue16
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue38
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss3
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss4
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb2
-rw-r--r--app/controllers/projects/learn_gitlab_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/experiments/application_experiment.rb7
-rw-r--r--app/experiments/new_project_readme_content_experiment.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb2
-rw-r--r--app/helpers/auth_helper.rb11
-rw-r--r--app/models/clusters/agent.rb6
-rw-r--r--app/models/clusters/agent_token.rb17
-rw-r--r--app/models/clusters/agents/activity_event.rb5
-rw-r--r--app/models/member.rb7
-rw-r--r--app/models/members/group_member.rb11
-rw-r--r--app/models/members/project_member.rb11
-rw-r--r--app/services/merge_requests/approval_service.rb1
-rw-r--r--app/services/merge_requests/base_service.rb14
-rw-r--r--app/services/merge_requests/bulk_remove_attention_requested_service.rb22
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/handle_assignees_change_service.rb2
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/remove_attention_requested_service.rb41
-rw-r--r--app/services/merge_requests/toggle_attention_requested_service.rb4
-rw-r--r--app/services/namespaces/invite_team_email_service.rb3
-rw-r--r--app/views/profiles/accounts/_providers.html.haml7
-rw-r--r--config/feature_flags/development/configure_iac_scanning_via_mr.yml8
-rw-r--r--doc/ci/yaml/index.md4
-rw-r--r--doc/development/experiment_guide/gitlab_experiment.md20
-rw-r--r--doc/integration/saml.md68
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md9
-rw-r--r--doc/user/project/merge_requests/commit_templates.md7
-rw-r--r--lib/api/discussions.rb7
-rw-r--r--lib/api/merge_request_diffs.rb2
-rw-r--r--lib/api/merge_requests.rb26
-rw-r--r--lib/api/notes.rb5
-rw-r--r--lib/api/resource_label_events.rb2
-rw-r--r--lib/api/resource_milestone_events.rb2
-rw-r--r--lib/api/resource_state_events.rb2
-rw-r--r--lib/api/suggestions.rb4
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml3
-rw-r--r--lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb11
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/merge_requests/commit_message_generator.rb4
-rw-r--r--lib/tasks/gitlab/db.rake4
-rw-r--r--locale/gitlab.pot8
-rw-r--r--spec/experiments/application_experiment_spec.rb8
-rw-r--r--spec/factories/sequences.rb2
-rw-r--r--spec/features/alert_management/alert_details_spec.rb4
-rw-r--r--spec/features/gitlab_experiments_spec.rb3
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb2
-rw-r--r--spec/frontend/flash_spec.js139
-rw-r--r--spec/frontend/issues/issue_spec.js (renamed from spec/frontend/issue_spec.js)2
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js44
-rw-r--r--spec/frontend/repository/mock_data.js8
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js56
-rw-r--r--spec/frontend/work_items/mock_data.js19
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js8
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js31
-rw-r--r--spec/helpers/auth_helper_spec.rb166
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb26
-rw-r--r--spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb47
-rw-r--r--spec/models/clusters/agent_spec.rb33
-rw-r--r--spec/models/clusters/agent_token_spec.rb32
-rw-r--r--spec/models/merge_request_spec.rb3
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb10
-rw-r--r--spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb45
-rw-r--r--spec/services/merge_requests/close_service_spec.rb4
-rw-r--r--spec/services/merge_requests/handle_assignees_change_service_spec.rb8
-rw-r--r--spec/services/merge_requests/remove_attention_requested_service_spec.rb85
-rw-r--r--spec/services/merge_requests/toggle_attention_requested_service_spec.rb16
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb6
111 files changed, 1260 insertions, 323 deletions
diff --git a/.ruby-version b/.ruby-version
index a4dd9dba4fb..a603bb50a29 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.7.4
+2.7.5
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a0263e681f1..de82148a9ff 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-3ef55853e9e161204464868390d97d1a1577042d
+fe6bcc9ca347b59714c46adf65d100dd93abde52
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index f71c896d82f..f0ef55f73eb 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -62,7 +62,7 @@ const createFlashEl = (message, type) => `
</div>
`;
-const removeFlashClickListener = (flashEl, fadeTransition) => {
+const addDismissFlashClickListener = (flashEl, fadeTransition) => {
// There are some flash elements which do not have a closeEl.
// https://gitlab.com/gitlab-org/gitlab/blob/763426ef344488972eb63ea5be8744e0f8459e6b/ee/app/views/layouts/header/_read_only_banner.html.haml
getCloseEl(flashEl)?.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
@@ -113,7 +113,7 @@ const createFlash = function createFlash({
}
}
- removeFlashClickListener(flashEl, fadeTransition);
+ addDismissFlashClickListener(flashEl, fadeTransition);
flashContainer.classList.add('gl-display-block');
@@ -130,9 +130,8 @@ const createFlash = function createFlash({
export {
createFlash as default,
- createAction,
hideFlash,
- removeFlashClickListener,
+ addDismissFlashClickListener,
FLASH_TYPES,
FLASH_CLOSED_EVENT,
};
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/issuable/issuable_template_selector.js
index 1bb5e214c2e..cce903d388d 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/issuable/issuable_template_selector.js
@@ -1,9 +1,7 @@
-/* eslint-disable no-useless-return */
-
import $ from 'jquery';
+import TemplateSelector from '~/blob/template_selector';
import { __ } from '~/locale';
import Api from '../api';
-import TemplateSelector from '../blob/template_selector';
export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
@@ -109,7 +107,5 @@ export default class IssuableTemplateSelector extends TemplateSelector {
} else {
this.setEditorContent(this.currentTemplate, { skipFocus: false });
}
-
- return;
}
}
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js b/app/assets/javascripts/issuable/issuable_template_selectors.js
index 443b3084113..92f825e55d3 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js
+++ b/app/assets/javascripts/issuable/issuable_template_selectors.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new, class-methods-use-this */
-
import $ from 'jquery';
import IssuableTemplateSelector from './issuable_template_selector';
@@ -10,6 +8,8 @@ export default class IssuableTemplateSelectors {
this.$dropdowns.each((i, dropdown) => {
const $dropdown = $(dropdown);
+
+ // eslint-disable-next-line no-new
new IssuableTemplateSelector({
pattern: /(\.md)/,
data: $dropdown.data('data'),
@@ -21,6 +21,7 @@ export default class IssuableTemplateSelectors {
});
}
+ // eslint-disable-next-line class-methods-use-this
initEditor() {
const editor = $('.markdown-area');
// Proxy ace-editor's .setValue to jQuery's .val
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/issues/filtered_search_service_desk.js
index bec207aa439..bec207aa439 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
+++ b/app/assets/javascripts/issues/filtered_search_service_desk.js
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/issues/form.js
index adccdda3475..20a8c251304 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/issues/form.js
@@ -8,7 +8,7 @@ import initSuggestions from '~/issues/suggestions';
import initIssuableTypeSelector from '~/issues/type_selector';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
-import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
+import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
export default () => {
new ShortcutsNavigation();
diff --git a/app/assets/javascripts/issues/init_filtered_search_service_desk.js b/app/assets/javascripts/issues/init_filtered_search_service_desk.js
new file mode 100644
index 00000000000..1901802c11c
--- /dev/null
+++ b/app/assets/javascripts/issues/init_filtered_search_service_desk.js
@@ -0,0 +1,11 @@
+import FilteredSearchServiceDesk from './filtered_search_service_desk';
+
+export function initFilteredSearchServiceDesk() {
+ if (document.querySelector('.filtered-search')) {
+ const supportBotData = JSON.parse(
+ document.querySelector('.js-service-desk-issues').dataset.supportBot,
+ );
+ const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+ filteredSearchManager.setup();
+ }
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issues/issue.js
index 1e053d7daaa..c471875654b 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import { joinPaths } from '~/lib/utils/url_utility';
-import CreateMergeRequestDropdown from './create_merge_request_dropdown';
-import createFlash from './flash';
-import { EVENT_ISSUABLE_VUE_APP_CHANGE } from './issuable/constants';
-import axios from './lib/utils/axios_utils';
-import { addDelimiter } from './lib/utils/text_utility';
-import { __ } from './locale';
+import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
+import createFlash from '~/flash';
+import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
+import axios from '~/lib/utils/axios_utils';
+import { addDelimiter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
export default class Issue {
constructor() {
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/issues/manual_ordering.js
index 9613246d6a6..9613246d6a6 100644
--- a/app/assets/javascripts/manual_ordering.js
+++ b/app/assets/javascripts/issues/manual_ordering.js
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/issues/show.js
index 5a48f463acc..33b1c47b4fe 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/issues/show.js
@@ -2,7 +2,7 @@ import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/issuable/init_issuable_sidebar';
import { IssuableType } from '~/vue_shared/issuable/show/constants';
-import Issue from '~/issue';
+import Issue from '~/issues/issue';
import { initIncidentApp, initIncidentHeaderActions } from '~/issues/show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issues/show/issue';
import { parseIssuableData } from '~/issues/show/utils/parse_data';
diff --git a/app/assets/javascripts/issues/show/components/fields/description_template.vue b/app/assets/javascripts/issues/show/components/fields/description_template.vue
index 45a05a97d6e..9ce49b65a1a 100644
--- a/app/assets/javascripts/issues/show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description_template.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
-import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
+import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
export default {
components: {
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
index 73dba056e85..516a48aaa5b 100644
--- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -11,7 +11,7 @@ import axios from '~/lib/utils/axios_utils';
import { scrollToElement, historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams, queryToObject, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import initManualOrdering from '~/manual_ordering';
+import initManualOrdering from '~/issues/manual_ordering';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
sortOrderMap,
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index e422d9b1a32..251086363c7 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -16,7 +16,7 @@ import * as popovers from '~/popovers';
import * as tooltips from '~/tooltips';
import { initHeaderSearchApp } from '~/header_search';
import initAlertHandler from './alert_handler';
-import { removeFlashClickListener } from './flash';
+import { addDismissFlashClickListener } from './flash';
import initTodoToggle from './header';
import initLayoutNav from './layout_nav';
import { logHelloDeferred } from './lib/logger/hello_deferred';
@@ -259,7 +259,7 @@ if (flashContainer && flashContainer.children.length) {
flashContainer
.querySelectorAll('.flash-alert, .flash-notice, .flash-success')
.forEach((flashEl) => {
- removeFlashClickListener(flashEl);
+ addDismissFlashClickListener(flashEl);
});
}
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index f36b9fdc60a..d0903ad53bc 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -1,5 +1,5 @@
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import initManualOrdering from '~/manual_ordering';
+import initManualOrdering from '~/issues/manual_ordering';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 623e665ff86..966d55e5587 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,7 +1,7 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
-import initManualOrdering from '~/manual_ordering';
+import initManualOrdering from '~/issues/manual_ordering';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index a75b68873ef..4633eaef8f9 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -1,6 +1,6 @@
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '../../issues/show';
+import initShow from '~/issues/show';
initShow();
initSidebarBundle();
diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js
index 48afd2142ee..aa00d1f58bd 100644
--- a/app/assets/javascripts/pages/projects/issues/edit/index.js
+++ b/app/assets/javascripts/pages/projects/issues/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from 'ee_else_ce/pages/projects/issues/form';
+import initForm from 'ee_else_ce/issues/form';
initForm();
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index e35a48e3474..d34536015e0 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -4,7 +4,7 @@ import initCsvImportExportButtons from '~/issuable/init_csv_import_export_button
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
import IssuableIndex from '~/issuable/issuable_index';
import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
-import initManualOrdering from '~/manual_ordering';
+import initManualOrdering from '~/issues/manual_ordering';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js
index 48afd2142ee..aa00d1f58bd 100644
--- a/app/assets/javascripts/pages/projects/issues/new/index.js
+++ b/app/assets/javascripts/pages/projects/issues/new/index.js
@@ -1,3 +1,3 @@
-import initForm from 'ee_else_ce/pages/projects/issues/form';
+import initForm from 'ee_else_ce/issues/form';
initForm();
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index d906c579697..69639d17f8a 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,14 +1,7 @@
import { mountIssuablesListApp } from '~/issues_list';
-import FilteredSearchServiceDesk from './filtered_search';
+import { initFilteredSearchServiceDesk } from '~/issues/init_filtered_search_service_desk';
-const supportBotData = JSON.parse(
- document.querySelector('.js-service-desk-issues').dataset.supportBot,
-);
-
-if (document.querySelector('.filtered-search')) {
- const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
- filteredSearchManager.setup();
-}
+initFilteredSearchServiceDesk();
if (gon.features?.vueIssuablesList) {
mountIssuablesListApp();
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 1282d2aa303..d0b1942f2a4 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,7 +1,7 @@
import { store } from '~/notes/stores';
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '../show';
+import initShow from '~/issues/show';
initShow();
initSidebarBundle(store);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 49197881731..ebf7c266482 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -7,7 +7,7 @@ import Diff from '~/diff';
import GLForm from '~/gl_form';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
-import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
+import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
export default () => {
new Diff();
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index cea95645fa4..f3fa4526999 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -157,11 +157,18 @@ export default {
},
canLock() {
const { pushCode, downloadCode } = this.project.userPermissions;
+ const currentUsername = window.gon?.current_username;
+
+ if (this.pathLockedByUser && this.pathLockedByUser.username !== currentUsername) {
+ return false;
+ }
return pushCode && downloadCode;
},
- isLocked() {
- return this.project.pathLocks.nodes.some((node) => node.path === this.path);
+ pathLockedByUser() {
+ const pathLock = this.project.pathLocks.nodes.find((node) => node.path === this.path);
+
+ return pathLock ? pathLock.user : null;
},
showForkSuggestion() {
const { createMergeRequestIn, forkProject } = this.project.userPermissions;
@@ -270,7 +277,7 @@ export default {
:can-push-to-branch="blobInfo.canCurrentUserPushToBranch"
:empty-repo="project.repository.empty"
:project-path="projectPath"
- :is-locked="isLocked"
+ :is-locked="Boolean(pathLockedByUser)"
:can-lock="canLock"
/>
</template>
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 1679d6f9f5d..45d1ba80917 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -11,6 +11,10 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
nodes {
id
path
+ user {
+ id
+ username
+ }
}
}
repository {
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 9c80506549e..bc318262b27 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -156,27 +156,23 @@ export const securityFeatures = [
// https://gitlab.com/gitlab-org/gitlab/-/issues/331621
canEnableByMergeRequest: true,
},
- ...(gon?.features?.configureIacScanningViaMr
- ? [
- {
- name: SAST_IAC_NAME,
- shortName: SAST_IAC_SHORT_NAME,
- description: SAST_IAC_DESCRIPTION,
- helpPath: SAST_IAC_HELP_PATH,
- configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
- type: REPORT_TYPE_SAST_IAC,
+ {
+ name: SAST_IAC_NAME,
+ shortName: SAST_IAC_SHORT_NAME,
+ description: SAST_IAC_DESCRIPTION,
+ helpPath: SAST_IAC_HELP_PATH,
+ configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST_IAC,
- // This field is currently hardcoded because SAST IaC is always available.
- // It will eventually come from the Backend, the progress is tracked in
- // https://gitlab.com/gitlab-org/gitlab/-/issues/331622
- available: true,
+ // This field is currently hardcoded because SAST IaC is always available.
+ // It will eventually come from the Backend, the progress is tracked in
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/331622
+ available: true,
- // This field will eventually come from the backend, the progress is
- // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
- canEnableByMergeRequest: true,
- },
- ]
- : []),
+ // This field will eventually come from the backend, the progress is
+ // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
+ canEnableByMergeRequest: true,
+ },
{
name: DAST_NAME,
shortName: DAST_SHORT_NAME,
@@ -278,21 +274,17 @@ export const featureToMutationMap = {
},
}),
},
- ...(gon?.features?.configureIacScanningViaMr
- ? {
- [REPORT_TYPE_SAST_IAC]: {
- mutationId: 'configureSastIac',
- getMutationPayload: (projectPath) => ({
- mutation: configureSastIacMutation,
- variables: {
- input: {
- projectPath,
- },
- },
- }),
+ [REPORT_TYPE_SAST_IAC]: {
+ mutationId: 'configureSastIac',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSastIacMutation,
+ variables: {
+ input: {
+ projectPath,
},
- }
- : {}),
+ },
+ }),
+ },
[REPORT_TYPE_SECRET_DETECTION]: {
mutationId: 'configureSecretDetection',
getMutationPayload: (projectPath) => ({
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index 5dc93476120..86e46016534 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -5,7 +5,7 @@ import {
GlLoadingIcon,
GlTooltip,
GlSprintf,
- GlLink,
+ GlButton,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
@@ -20,7 +20,7 @@ export default {
GlSprintf,
GlDropdown,
GlDropdownItem,
- GlLink,
+ GlButton,
SeverityToken,
},
inject: ['canUpdate'],
@@ -150,23 +150,25 @@ export default {
<div class="hide-collapsed">
<p
- class="gl-line-height-20 gl-mb-0 gl-text-gray-900 gl-display-flex gl-justify-content-space-between"
+ class="gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-display-flex gl-justify-content-space-between"
>
{{ $options.i18n.SEVERITY }}
- <gl-link
+ <gl-button
v-if="canUpdate"
+ category="tertiary"
+ size="small"
data-testid="editButton"
- href="#"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
{{ $options.i18n.EDIT }}
- </gl-link>
+ </gl-button>
</p>
<gl-dropdown
:class="dropdownClass"
block
+ :header-text="__('Assign severity')"
:text="selectedItem.label"
toggle-class="dropdown-menu-toggle gl-mb-2"
@keydown.esc.native="hideDropdown"
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 86580744ccc..a49ddac8c89 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -79,6 +79,20 @@ export default class SidebarMediator {
}),
);
} else {
+ const currentUserId = gon.current_user_id;
+
+ if (currentUserId !== user.id) {
+ const currentUserReviewerOrAssignee = isReviewer
+ ? this.store.findReviewer({ id: currentUserId })
+ : this.store.findAssignee({ id: currentUserId });
+
+ if (currentUserReviewerOrAssignee?.attention_requested) {
+ // Update current users attention_requested state
+ this.store.updateReviewer(currentUserId, 'attention_requested');
+ this.store.updateAssignee(currentUserId, 'attention_requested');
+ }
+ }
+
toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
}
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index c24318cb9ad..489d4afa41f 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -220,16 +220,17 @@ export default {
class="gl-text-gray-900 gl-mb-2 gl-line-height-20 gl-display-flex gl-justify-content-space-between"
>
{{ __('Assignee') }}
- <a
+ <gl-button
v-if="isEditable"
ref="editButton"
- class="btn-link"
- href="#"
+ category="tertiary"
+ size="small"
+ class="gl-text-black-normal!"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
{{ __('Edit') }}
- </a>
+ </gl-button>
</p>
<gl-dropdown
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index eaa5fc5af04..c512585b980 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -100,7 +100,8 @@ export default {
<gl-button
v-if="isEditable"
class="gl-text-black-normal!"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleFormDropdown"
@keydown.esc="hideDropdown"
>
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
new file mode 100644
index 00000000000..5e9e50a94f0
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -0,0 +1,71 @@
+<script>
+import { escape } from 'lodash';
+import { __ } from '~/locale';
+
+export default {
+ props: {
+ initialTitle: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ placeholder: {
+ type: String,
+ required: false,
+ default: __('Add a title...'),
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ title: this.initialTitle,
+ };
+ },
+ methods: {
+ getSanitizedTitle(inputEl) {
+ const { innerText } = inputEl;
+ return escape(innerText);
+ },
+ handleBlur({ target }) {
+ this.$emit('title-changed', this.getSanitizedTitle(target));
+ },
+ handleInput({ target }) {
+ this.$emit('title-input', this.getSanitizedTitle(target));
+ },
+ handleSubmit() {
+ this.$refs.titleEl.blur();
+ },
+ },
+};
+</script>
+
+<template>
+ <h2
+ class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block"
+ :class="{ 'gl-cursor-not-allowed': disabled }"
+ data-testid="title"
+ aria-labelledby="item-title"
+ >
+ <span
+ id="item-title"
+ ref="titleEl"
+ role="textbox"
+ :aria-label="__('Title')"
+ :data-placeholder="placeholder"
+ :contenteditable="!disabled"
+ class="gl-pseudo-placeholder"
+ @blur="handleBlur"
+ @keyup="handleInput"
+ @keydown.enter.exact="handleSubmit"
+ @keydown.ctrl.u.prevent
+ @keydown.meta.u.prevent
+ @keydown.ctrl.b.prevent
+ @keydown.meta.b.prevent
+ >{{ title }}</span
+ >
+ </h2>
+</template>
diff --git a/app/assets/javascripts/work_items/graphql/resolvers.js b/app/assets/javascripts/work_items/graphql/resolvers.js
index bab0147f4b8..8005f334314 100644
--- a/app/assets/javascripts/work_items/graphql/resolvers.js
+++ b/app/assets/javascripts/work_items/graphql/resolvers.js
@@ -29,5 +29,30 @@ export const resolvers = {
workItem,
};
},
+
+ updateWorkItem(_, { input }, { cache }) {
+ const workItemTitle = {
+ __typename: 'TitleWidget',
+ type: 'TITLE',
+ enabled: true,
+ contentText: input.title,
+ };
+ const workItem = {
+ __typename: 'WorkItem',
+ type: 'FEATURE',
+ id: input.id,
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [workItemTitle],
+ },
+ };
+
+ cache.writeQuery({ query: workItemQuery, variables: { id: input.id }, data: { workItem } });
+
+ return {
+ __typename: 'UpdateWorkItemPayload',
+ workItem,
+ };
+ },
},
};
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index 2a9cd52c18e..dd7ea7c26cc 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -37,14 +37,24 @@ type CreateWorkItemInput {
title: String!
}
+type UpdateWorkItemInput {
+ id: ID!
+ title: String
+}
+
type CreateWorkItemPayload {
workItem: WorkItem!
}
+type UpdateWorkItemPayload {
+ workItem: WorkItem!
+}
+
extend type Query {
workItem(id: ID!): WorkItem!
}
extend type Mutation {
createWorkItem(input: CreateWorkItemInput!): CreateWorkItemPayload!
+ updateWorkItem(input: UpdateWorkItemInput!): UpdateWorkItemPayload!
}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
new file mode 100644
index 00000000000..fc140954fbe
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
@@ -0,0 +1,18 @@
+#import './widget.fragment.graphql'
+
+mutation updateWorkItem($input: UpdateWorkItemInput) {
+ updateWorkItem(input: $input) @client {
+ workItem {
+ id
+ type
+ widgets {
+ nodes {
+ ...WidgetBase
+ ... on TitleWidget {
+ contentText
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 190e50f903c..43cbee019c1 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -2,10 +2,13 @@
import { GlButton, GlAlert } from '@gitlab/ui';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
+import ItemTitle from '../components/item_title.vue';
+
export default {
components: {
GlButton,
GlAlert,
+ ItemTitle,
},
data() {
return {
@@ -37,6 +40,9 @@ export default {
this.error = true;
}
},
+ handleTitleInput(title) {
+ this.title = title;
+ },
},
};
</script>
@@ -46,15 +52,7 @@ export default {
<gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
__('Something went wrong when creating a work item. Please try again')
}}</gl-alert>
- <label for="title" class="gl-sr-only">{{ __('Title') }}</label>
- <input
- id="title"
- v-model.trim="title"
- type="text"
- class="gl-font-size-h-display gl-font-weight-bold gl-my-5 gl-border-none gl-w-full gl-pl-2"
- data-testid="title-input"
- :placeholder="__('Add a title…')"
- />
+ <item-title data-testid="title-input" @title-input="handleTitleInput" />
<div class="gl-bg-gray-10 gl-py-5 gl-px-6">
<gl-button
variant="confirm"
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index 493ee0aba01..479274baf3a 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -1,8 +1,16 @@
<script>
+import { GlAlert } from '@gitlab/ui';
import workItemQuery from '../graphql/work_item.query.graphql';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { widgetTypes } from '../constants';
+import ItemTitle from '../components/item_title.vue';
+
export default {
+ components: {
+ ItemTitle,
+ GlAlert,
+ },
props: {
id: {
type: String,
@@ -12,6 +20,7 @@ export default {
data() {
return {
workItem: null,
+ error: false,
};
},
apollo: {
@@ -29,20 +38,39 @@ export default {
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
},
},
+ methods: {
+ async updateWorkItem(title) {
+ try {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.id,
+ title,
+ },
+ },
+ });
+ } catch {
+ this.error = true;
+ }
+ },
+ },
};
</script>
<template>
<section>
+ <gl-alert v-if="error" variant="danger" @dismiss="error = false">{{
+ __('Something went wrong while updating work item. Please try again')
+ }}</gl-alert>
<!-- Title widget placeholder -->
<div>
- <h2
+ <item-title
v-if="titleWidgetData"
- class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5"
+ :initial-title="titleWidgetData.contentText"
data-testid="title"
- >
- {{ titleWidgetData.contentText }}
- </h2>
+ @title-changed="updateWorkItem"
+ />
</div>
</section>
</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 354d2737894..36a0d3ca3ca 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -479,6 +479,13 @@ img.emoji {
border-top: 1px solid $border-color;
}
+.gl-pseudo-placeholder:empty::before {
+ content: attr(data-placeholder);
+ font-weight: $gl-font-weight-normal;
+ color: $gl-text-color-secondary;
+ cursor: text;
+}
+
/**
🚨 Do not use these classes — they clash with the Gitlab UI design system and will be removed. 🚨
See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 0680de32e86..9914e573247 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -1831,6 +1831,9 @@ body.gl-dark .navbar-gitlab .search form:active {
background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px var(--blue-200);
}
+body.gl-dark .navbar-gitlab .search form .search-input {
+ color: var(--gl-text-color);
+}
body.gl-dark {
--gray-10: #1f1f1f;
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 2b5751cab36..bb9a9cf0497 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -122,6 +122,10 @@ body.gl-dark {
background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px var(--blue-200);
}
+
+ .search-input {
+ color: var(--gl-text-color);
+ }
}
}
}
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 1696eef09a8..dc5b22e1606 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -9,7 +9,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
after_action :verify_known_sign_in
- protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true
+ protect_from_forgery except: [:kerberos, :saml, :cas3, :failure] + AuthHelper.saml_providers, with: :exception, prepend: true
feature_category :authentication_and_authorization
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 392a6afc636..6f12e3940dd 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -23,7 +23,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
def setup_walkthrough_experiment
experiment(:pipeline_editor_walkthrough, namespace: @project.namespace, sticky_to: current_user) do |e|
e.candidate {}
- e.record!
+ e.publish_to_database
end
end
end
diff --git a/app/controllers/projects/learn_gitlab_controller.rb b/app/controllers/projects/learn_gitlab_controller.rb
index d5675618da5..177533b89c8 100644
--- a/app/controllers/projects/learn_gitlab_controller.rb
+++ b/app/controllers/projects/learn_gitlab_controller.rb
@@ -21,7 +21,7 @@ class Projects::LearnGitlabController < Projects::ApplicationController
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
e.candidate {}
- e.record!
+ e.publish_to_database
end
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index d49539c56fb..79935300fb6 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -312,7 +312,7 @@ class Projects::PipelinesController < Projects::ApplicationController
e.control {}
e.candidate {}
- e.record!
+ e.publish_to_database
end
end
@@ -325,7 +325,7 @@ class Projects::PipelinesController < Projects::ApplicationController
e.control {}
e.candidate {}
- e.record!
+ e.publish_to_database
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 36f69028d6a..04c40826d13 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -38,6 +38,7 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
+ push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
end
layout :determine_layout
diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb
index 32dd6960929..bf8d43de6aa 100644
--- a/app/experiments/application_experiment.rb
+++ b/app/experiments/application_experiment.rb
@@ -13,7 +13,6 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
super
publish_to_client
- publish_to_database if @record
end
def publish_to_client
@@ -25,6 +24,8 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
end
def publish_to_database
+ ActiveSupport::Deprecation.warn('publish_to_database is deprecated and should not be used for reporting anymore')
+
return unless should_track?
# if the context contains a namespace, group, project, user, or actor
@@ -36,10 +37,6 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
Experiment.add_subject(name, variant: variant_name || :control, subject: subject)
end
- def record!
- @record = true
- end
-
def control_behavior
# define a default nil control behavior so we can omit it when not needed
end
diff --git a/app/experiments/new_project_readme_content_experiment.rb b/app/experiments/new_project_readme_content_experiment.rb
index d9f0fb3b93e..1de7632268d 100644
--- a/app/experiments/new_project_readme_content_experiment.rb
+++ b/app/experiments/new_project_readme_content_experiment.rb
@@ -6,7 +6,7 @@ class NewProjectReadmeContentExperiment < ApplicationExperiment # rubocop:disabl
def run_with(project, variant: nil)
@project = project
- record!
+ publish_to_database
run(variant)
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index a0f00ddc3c6..8ad313758e5 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -96,7 +96,7 @@ module Types
description: 'Rebase commit SHA of the merge request.'
field :rebase_in_progress, GraphQL::Types::Boolean, method: :rebase_in_progress?, null: false, calls_gitaly: true,
description: 'Indicates if there is a rebase currently in progress for the merge request.'
- field :default_merge_commit_message, GraphQL::Types::String, null: true,
+ field :default_merge_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default merge commit message of the merge request.'
field :default_merge_commit_message_with_description, GraphQL::Types::String, null: true,
description: 'Default merge commit message of the merge request with description. Will have the same value as `defaultMergeCommitMessage` when project has `mergeCommitTemplate` set.',
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 2032b7e8bb7..c1a74382d46 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -86,6 +86,17 @@ module AuthHelper
auth_providers.select { |provider| form_based_provider?(provider) }
end
+ def saml_providers
+ auth_providers.select { |provider| auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML' }
+ end
+
+ def auth_strategy_class(provider)
+ config = Gitlab::Auth::OAuth::Provider.config_for(provider)
+ return if config.nil? || config['args'].blank?
+
+ config.args['strategy_class']
+ end
+
def any_form_based_providers_enabled?
form_based_providers.any? { |provider| form_enabled_for_sign_in?(provider) }
end
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index f04ac6f1722..98490a13351 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -4,6 +4,8 @@ module Clusters
class Agent < ApplicationRecord
self.table_name = 'cluster_agents'
+ INACTIVE_AFTER = 1.hour.freeze
+
belongs_to :created_by_user, class_name: 'User', optional: true
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
@@ -33,5 +35,9 @@ module Clusters
def has_access_to?(requested_project)
requested_project == project
end
+
+ def active?
+ agent_tokens.where("last_used_at > ?", INACTIVE_AFTER.ago).exists?
+ end
end
end
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index 27a3cd8d13d..87dba50cd69 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -28,8 +28,12 @@ module Clusters
cache_attributes(track_values)
- # Use update_column so updated_at is skipped
- update_columns(track_values) if can_update_track_values?
+ if can_update_track_values?
+ log_activity_event!(track_values[:last_used_at]) unless agent.active?
+
+ # Use update_column so updated_at is skipped
+ update_columns(track_values)
+ end
end
private
@@ -44,5 +48,14 @@ module Clusters
real_last_used_at.nil? ||
(Time.current - real_last_used_at) >= last_used_at_max_age
end
+
+ def log_activity_event!(recorded_at)
+ agent.activity_events.create!(
+ kind: :agent_connected,
+ level: :info,
+ recorded_at: recorded_at,
+ agent_token: self
+ )
+ end
end
end
diff --git a/app/models/clusters/agents/activity_event.rb b/app/models/clusters/agents/activity_event.rb
index 668aba74821..5d9c885c923 100644
--- a/app/models/clusters/agents/activity_event.rb
+++ b/app/models/clusters/agents/activity_event.rb
@@ -18,7 +18,10 @@ module Clusters
nullify_if_blank :detail
enum kind: {
- token_created: 0
+ token_created: 0,
+ token_revoked: 1,
+ agent_connected: 2,
+ agent_disconnected: 3
}, _prefix: true
enum level: {
diff --git a/app/models/member.rb b/app/models/member.rb
index a2d53f006d4..90fb281abf4 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -52,6 +52,7 @@ class Member < ApplicationRecord
message: _('project bots cannot be added to other groups / projects')
},
if: :project_bot?
+ validate :access_level_inclusion
scope :with_invited_user_state, -> do
joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
@@ -382,6 +383,12 @@ class Member < ApplicationRecord
private
+ def access_level_inclusion
+ return if access_level.in?(Gitlab::Access.all_values)
+
+ errors.add(:access_level, "is not included in the list")
+ end
+
def send_invite
# override in subclass
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 9062a405218..1ad4cb6d368 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -6,6 +6,7 @@ class GroupMember < Member
include CreatedAtFilterable
SOURCE_TYPE = 'Namespace'
+ SOURCE_TYPE_FORMAT = /\ANamespace\z/.freeze
belongs_to :group, foreign_key: 'source_id'
alias_attribute :namespace_id, :source_id
@@ -13,9 +14,7 @@ class GroupMember < Member
# Make sure group member points only to group as it source
default_value_for :source_type, SOURCE_TYPE
- validates :source_type, format: { with: /\ANamespace\z/ }
- validates :access_level, presence: true
- validate :access_level_inclusion
+ validates :source_type, format: { with: SOURCE_TYPE_FORMAT }
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
@@ -65,12 +64,6 @@ class GroupMember < Member
super
end
- def access_level_inclusion
- return if access_level.in?(Gitlab::Access.all_values)
-
- errors.add(:access_level, "is not included in the list")
- end
-
def send_invite
run_after_commit_or_now { notification_service.invite_group_member(self, @raw_invite_token) }
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 89b72508e84..6fc665cb87a 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -3,6 +3,7 @@
class ProjectMember < Member
extend ::Gitlab::Utils::Override
SOURCE_TYPE = 'Project'
+ SOURCE_TYPE_FORMAT = /\AProject\z/.freeze
belongs_to :project, foreign_key: 'source_id'
@@ -10,8 +11,7 @@ class ProjectMember < Member
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
- validates :source_type, format: { with: /\AProject\z/ }
- validates :access_level, inclusion: { in: Gitlab::Access.values }
+ validates :source_type, format: { with: SOURCE_TYPE_FORMAT }
default_scope { where(source_type: SOURCE_TYPE) } # rubocop:disable Cop/DefaultScope
scope :in_project, ->(project) { where(source_id: project.id) }
@@ -92,6 +92,13 @@ class ProjectMember < Member
private
+ override :access_level_inclusion
+ def access_level_inclusion
+ return if access_level.in?(Gitlab::Access.values)
+
+ errors.add(:access_level, "is not included in the list")
+ end
+
override :refresh_member_authorized_projects
def refresh_member_authorized_projects(blocking:)
return unless user
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 62e599e3e27..3f39b2742c6 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -14,6 +14,7 @@ module MergeRequests
create_approval_note(merge_request)
mark_pending_todos_as_done(merge_request)
execute_approval_hooks(merge_request, current_user)
+ remove_attention_requested(merge_request, current_user)
merge_request_activity_counter.track_approve_mr_action(user: current_user)
success
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 0a652c58aab..d744881549a 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -58,6 +58,8 @@ module MergeRequests
new_reviewers = merge_request.reviewers - old_reviewers
merge_request_activity_counter.track_users_review_requested(users: new_reviewers)
merge_request_activity_counter.track_reviewers_changed_action(user: current_user)
+
+ remove_attention_requested(merge_request, current_user)
end
def cleanup_environments(merge_request)
@@ -238,6 +240,18 @@ module MergeRequests
Milestones::MergeRequestsCountService.new(milestone).delete_cache
end
+
+ def remove_all_attention_requests(merge_request)
+ return unless merge_request.attention_requested_enabled?
+
+ ::MergeRequests::BulkRemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request).execute
+ end
+
+ def remove_attention_requested(merge_request, user)
+ return unless merge_request.attention_requested_enabled?
+
+ ::MergeRequests::RemoveAttentionRequestedService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, user: user).execute
+ end
end
end
diff --git a/app/services/merge_requests/bulk_remove_attention_requested_service.rb b/app/services/merge_requests/bulk_remove_attention_requested_service.rb
new file mode 100644
index 00000000000..dd2ff741ba6
--- /dev/null
+++ b/app/services/merge_requests/bulk_remove_attention_requested_service.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class BulkRemoveAttentionRequestedService < MergeRequests::BaseService
+ attr_accessor :merge_request
+
+ def initialize(project:, current_user:, merge_request:)
+ super(project: project, current_user: current_user)
+
+ @merge_request = merge_request
+ end
+
+ def execute
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ merge_request.merge_request_assignees.update_all(state: :reviewed)
+ merge_request.merge_request_reviewers.update_all(state: :reviewed)
+
+ success
+ end
+ end
+end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index f83b14c7269..e9b253129b4 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -17,6 +17,7 @@ module MergeRequests
create_note(merge_request)
notification_service.async.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
+ remove_all_attention_requests(merge_request)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
merge_request.update_project_counter_caches
diff --git a/app/services/merge_requests/handle_assignees_change_service.rb b/app/services/merge_requests/handle_assignees_change_service.rb
index 87cd6544406..1d9f7ab59f4 100644
--- a/app/services/merge_requests/handle_assignees_change_service.rb
+++ b/app/services/merge_requests/handle_assignees_change_service.rb
@@ -22,6 +22,8 @@ module MergeRequests
merge_request_activity_counter.track_assignees_changed_action(user: current_user)
execute_assignees_hooks(merge_request, old_assignees) if options[:execute_hooks]
+
+ remove_attention_requested(merge_request, current_user)
end
private
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index ea3071b3c2d..e475b57e4a2 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -28,6 +28,7 @@ module MergeRequests
notification_service.merge_mr(merge_request, current_user)
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
merge_request.update_project_counter_caches
+ remove_all_attention_requests(merge_request)
delete_non_latest_diffs(merge_request)
cancel_review_app_jobs!(merge_request)
cleanup_environments(merge_request)
diff --git a/app/services/merge_requests/remove_attention_requested_service.rb b/app/services/merge_requests/remove_attention_requested_service.rb
new file mode 100644
index 00000000000..b727c24415e
--- /dev/null
+++ b/app/services/merge_requests/remove_attention_requested_service.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class RemoveAttentionRequestedService < MergeRequests::BaseService
+ attr_accessor :merge_request, :user
+
+ def initialize(project:, current_user:, merge_request:, user:)
+ super(project: project, current_user: current_user)
+
+ @merge_request = merge_request
+ @user = user
+ end
+
+ def execute
+ return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request)
+
+ if reviewer || assignee
+ update_state(reviewer)
+ update_state(assignee)
+
+ success
+ else
+ error("User is not a reviewer or assignee of the merge request")
+ end
+ end
+
+ private
+
+ def assignee
+ merge_request.find_assignee(user)
+ end
+
+ def reviewer
+ merge_request.find_reviewer(user)
+ end
+
+ def update_state(reviewer_or_assignee)
+ reviewer_or_assignee&.update(state: :reviewed)
+ end
+ end
+end
diff --git a/app/services/merge_requests/toggle_attention_requested_service.rb b/app/services/merge_requests/toggle_attention_requested_service.rb
index fd24e87454c..d9f81ac310f 100644
--- a/app/services/merge_requests/toggle_attention_requested_service.rb
+++ b/app/services/merge_requests/toggle_attention_requested_service.rb
@@ -21,6 +21,10 @@ module MergeRequests
if reviewer&.attention_requested? || assignee&.attention_requested?
create_attention_request_note
notity_user
+
+ if current_user.id != user.id
+ remove_attention_requested(merge_request, current_user)
+ end
else
create_remove_attention_request_note
end
diff --git a/app/services/namespaces/invite_team_email_service.rb b/app/services/namespaces/invite_team_email_service.rb
index 45975d1953a..78edc205990 100644
--- a/app/services/namespaces/invite_team_email_service.rb
+++ b/app/services/namespaces/invite_team_email_service.rb
@@ -29,13 +29,12 @@ module Namespaces
return if email_for_track_sent_to_user?
experiment(:invite_team_email, group: group) do |e|
+ e.publish_to_database
e.candidate do
send_email(user, group)
sent_email_records.add(user, track, series)
sent_email_records.save!
end
-
- e.record!
end
end
diff --git a/app/views/profiles/accounts/_providers.html.haml b/app/views/profiles/accounts/_providers.html.haml
index 5c0044ed825..73a437a0702 100644
--- a/app/views/profiles/accounts/_providers.html.haml
+++ b/app/views/profiles/accounts/_providers.html.haml
@@ -6,11 +6,13 @@
- providers.each do |provider|
- unlink_allowed = unlink_provider_allowed?(provider)
- link_allowed = link_provider_allowed?(provider)
+ - has_icon = provider_has_icon?(provider)
- if unlink_allowed || link_allowed
- if auth_active?(provider)
- if unlink_allowed
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: button_class do
- .social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
+ - if has_icon
+ .social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
.gl-button-text
= s_('Profiles|Disconnect %{provider}') % { provider: label_for_provider(provider) }
- else
@@ -19,7 +21,8 @@
= s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) }
- elsif link_allowed
= link_to omniauth_authorize_path(:user, provider), method: :post, class: button_class do
- .social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
+ - if has_icon
+ .social-provider-btn-image.gl-button-icon= provider_image_tag(provider)
.gl-button-text
= s_('Profiles|Connect %{provider}') % { provider: label_for_provider(provider) }
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities
diff --git a/config/feature_flags/development/configure_iac_scanning_via_mr.yml b/config/feature_flags/development/configure_iac_scanning_via_mr.yml
deleted file mode 100644
index cef22644b8f..00000000000
--- a/config/feature_flags/development/configure_iac_scanning_via_mr.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: configure_iac_scanning_via_mr
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73155
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343966
-milestone: '14.5'
-type: development
-group: group::static analysis
-default_enabled: true
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index b16ea44f6c0..36b5c857cf8 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -2128,9 +2128,7 @@ from the latest pipeline that completed successfully.
**Possible inputs**:
-- `needs:project`: A full project path, including namespace and group. If the
- project is in the same group or namespace, you can omit them from the `project`
- keyword. For example: `project: group/project-name` or `project: project-name`.
+- `needs:project`: A full project path, including namespace and group.
- `job`: The job to download artifacts from.
- `ref`: The ref to download artifacts from.
- `artifacts`: Must be `true` to download artifacts.
diff --git a/doc/development/experiment_guide/gitlab_experiment.md b/doc/development/experiment_guide/gitlab_experiment.md
index d81ac5372e3..288823bb41f 100644
--- a/doc/development/experiment_guide/gitlab_experiment.md
+++ b/doc/development/experiment_guide/gitlab_experiment.md
@@ -394,26 +394,6 @@ You may be asked from time to time to track a specific record ID in experiments.
The approach is largely up to the PM and engineer creating the implementation.
No recommendations are provided here at this time.
-### Record experiment subjects
-
-Snowplow tracking of identifiable users or groups is prohibited, but you can still
-determine if an experiment is successful or not. We're allowed to record the ID of
-a namespace, project or user in our database. Therefore, we can tell the experiment
-to record their ID together with the assigned experiment variant in the
-`experiment_subjects` database table for later analysis.
-
-For the recording to work, the experiment's context must include a `namespace`,
-`group`, `project`, `user`, or `actor`.
-
-To record the experiment subject when you first assign a variant, call `record!` in
-the experiment's block:
-
-```ruby
-experiment(:pill_color, actor: current_user) do |e|
- e.record!
-end
-```
-
## Test with RSpec
This gem provides some RSpec helpers and custom matchers. These are in flux as of GitLab 13.10.
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index d5f01aeb3b4..70d6932b9eb 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -163,6 +163,74 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user
is returned to GitLab and signed in.
+### Use multiple SAML identity providers
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14361) in GitLab 14.6.
+
+You can configure GitLab to use multiple SAML identity providers if:
+
+- Each provider has a unique name set that matches a name set in `args`.
+- The providers' names are:
+ - Used in OmniAuth configuration for properties based on the provider name. For example, `allowBypassTwoFactor`, `allowSingleSignOn`, and
+ `syncProfileFromProvider`.
+ - Used for association to each existing user as an additional identity.
+- The `assertion_consumer_service_url` matches the provider name.
+- The `strategy_class` is explicitly set because it cannot be inferred from provider name.
+
+Example multiple providers configuration for Omnibus GitLab:
+
+```ruby
+gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml_1',
+ args: {
+ name: 'saml_1', # This is mandatory and must match the provider name
+ strategy_class: 'OmniAuth::Strategies::SAML'
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
+ ... # Put here all the required arguments similar to a single provider
+ },
+ label: 'Provider 1' # Differentiate the two buttons and providers in the UI
+ },
+ {
+ name: 'saml_2',
+ args: {
+ name: 'saml_2', # This is mandatory and must match the provider name
+ strategy_class: 'OmniAuth::Strategies::SAML'
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
+ ... # Put here all the required arguments similar to a single provider
+ },
+ label: 'Provider 2' # Differentiate the two buttons and providers in the UI
+ }
+]
+```
+
+Example providers configuration for installations from source:
+
+```yaml
+omniauth:
+ providers:
+ - {
+ name: 'saml_1',
+ args: {
+ name: 'saml_1', # This is mandatory and must match the provider name
+ strategy_class: 'OmniAuth::Strategies::SAML',
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
+ ... # Put here all the required arguments similar to a single provider
+ },
+ label: 'Provider 1' # Differentiate the two buttons and providers in the UI
+ }
+ - {
+ name: 'saml_2',
+ args: {
+ name: 'saml_2', # This is mandatory and must match the provider name
+ strategy_class: 'OmniAuth::Strategies::SAML',
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
+ ... # Put here all the required arguments similar to a single provider
+ },
+ label: 'Provider 2' # Differentiate the two buttons and providers in the UI
+ }
+```
+
### Notes on configuring your identity provider
When configuring a SAML app on the IdP, you need at least:
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index 243ff8ad76b..fc707c266f9 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -194,12 +194,13 @@ To set a limit on how long these sessions are valid:
## Limit the lifetime of SSH keys **(ULTIMATE SELF)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6 [with a flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`. Disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6 [with a flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`. Disabled by default.
+> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/346753) in GitLab 14.6.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available,
-ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`.
-On GitLab.com, this feature is not available. The feature is not ready for production use.
+On self-managed GitLab, by default this feature is available. To hide the feature,
+ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `ff_limit_ssh_key_lifetime`.
+On GitLab.com, this feature is not available.
Users can optionally specify a lifetime for
[SSH keys](../../../ssh/index.md).
diff --git a/doc/user/project/merge_requests/commit_templates.md b/doc/user/project/merge_requests/commit_templates.md
index abe17f03288..bffb66755e0 100644
--- a/doc/user/project/merge_requests/commit_templates.md
+++ b/doc/user/project/merge_requests/commit_templates.md
@@ -65,6 +65,9 @@ GitLab creates a squash commit message with this template:
## Supported variables in commit templates
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20263) in GitLab 14.5.
+> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/346805) `first_commit` and `first_multiline_commit` variables in GitLab 14.6.
+
Commit message templates support these variables:
| Variable | Description | Output example |
@@ -73,8 +76,10 @@ Commit message templates support these variables:
| `%{target_branch}` | The name of the branch that the changes are applied to. | `main` |
| `%{title}` | Title of the merge request. | `Fix tests and translations` |
| `%{issues}` | String with phrase `Closes <issue numbers>`. Contains all issues mentioned in the merge request description that match [issue closing patterns](../issues/managing_issues.md#closing-issues-automatically). Empty if no issues are mentioned. | `Closes #465, #190 and #400` |
-| `%{description}` | Description of the merge request. | `Merge request description.<br>Can be multiline.` |
+| `%{description}` | Description of the merge request. | `Merge request description.`<br>`Can be multiline.` |
| `%{reference}` | Reference to the merge request. | `group-name/project-name!72359` |
+| `%{first_commit}` | Full message of the first commit in merge request diff. | `Update README.md` |
+| `%{first_multiline_commit}` | Full message of the first commit that's not a merge commit and has more than one line in message body. Merge Request title if all commits aren't multiline. | `Update README.md`<br><br>`Improved project description in readme file.` |
Empty variables that are the only word in a line are removed, along with all newline characters preceding it.
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index cf4b2348458..0709a8c2036 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -8,6 +8,13 @@ module API
before { authenticate! }
+ urgency :low, [
+ '/projects/:id/merge_requests/:noteable_id/discussions',
+ '/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id',
+ '/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id/notes',
+ '/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id/notes/:note_id'
+ ]
+
Helpers::DiscussionsHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 8fa7138af42..87623568a04 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -38,7 +38,7 @@ module API
requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
end
- get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
+ get ":id/merge_requests/:merge_request_iid/versions/:version_id", urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present_cached merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull, cache_context: nil
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 12187e497ba..063fff4023a 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -134,7 +134,7 @@ module API
use :merge_requests_params
use :optional_scope_param
end
- get feature_category: :code_review do
+ get feature_category: :code_review, urgency: :low do
authenticate! unless params[:scope] == 'all'
validate_anonymous_search_access! if params[:search].present?
merge_requests = find_merge_requests
@@ -155,7 +155,7 @@ module API
optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects',
default: true
end
- get ":id/merge_requests", feature_category: :code_review do
+ get ":id/merge_requests", feature_category: :code_review, urgency: :low do
validate_anonymous_search_access! if declared_params[:search].present?
merge_requests = find_merge_requests(group_id: user_group.id, include_subgroups: true)
@@ -195,7 +195,7 @@ module API
use :merge_requests_params
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of merge requests'
end
- get ":id/merge_requests", feature_category: :code_review do
+ get ":id/merge_requests", feature_category: :code_review, urgency: :low do
authorize! :read_merge_request, user_project
validate_anonymous_search_access! if declared_params[:search].present?
@@ -222,7 +222,7 @@ module API
desc: 'The target project of the merge request defaults to the :id of the project'
use :optional_params
end
- post ":id/merge_requests", feature_category: :code_review do
+ post ":id/merge_requests", feature_category: :code_review, urgency: :low do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20770')
authorize! :create_merge_request_from, user_project
@@ -244,7 +244,7 @@ module API
params do
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
end
- delete ":id/merge_requests/:merge_request_iid", feature_category: :code_review do
+ delete ":id/merge_requests/:merge_request_iid", feature_category: :code_review, urgency: :low do
merge_request = find_project_merge_request(params[:merge_request_iid])
authorize!(:destroy_merge_request, merge_request)
@@ -263,7 +263,7 @@ module API
desc 'Get a single merge request' do
success Entities::MergeRequest
end
- get ':id/merge_requests/:merge_request_iid', feature_category: :code_review do
+ get ':id/merge_requests/:merge_request_iid', feature_category: :code_review, urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request,
@@ -279,7 +279,7 @@ module API
desc 'Get the participants of a merge request' do
success Entities::UserBasic
end
- get ':id/merge_requests/:merge_request_iid/participants', feature_category: :code_review do
+ get ':id/merge_requests/:merge_request_iid/participants', feature_category: :code_review, urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
participants = ::Kaminari.paginate_array(merge_request.participants)
@@ -290,7 +290,7 @@ module API
desc 'Get the commits of a merge request' do
success Entities::Commit
end
- get ':id/merge_requests/:merge_request_iid/commits', feature_category: :code_review do
+ get ':id/merge_requests/:merge_request_iid/commits', feature_category: :code_review, urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits =
@@ -371,7 +371,7 @@ module API
desc 'Show the merge request changes' do
success Entities::MergeRequestChanges
end
- get ':id/merge_requests/:merge_request_iid/changes', feature_category: :code_review do
+ get ':id/merge_requests/:merge_request_iid/changes', feature_category: :code_review, urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request,
@@ -422,7 +422,7 @@ module API
use :optional_params
at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
end
- put ':id/merge_requests/:merge_request_iid', feature_category: :code_review do
+ put ':id/merge_requests/:merge_request_iid', feature_category: :code_review, urgency: :low do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20772')
merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
@@ -454,7 +454,7 @@ module API
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end
- put ':id/merge_requests/:merge_request_iid/merge', feature_category: :code_review do
+ put ':id/merge_requests/:merge_request_iid/merge', feature_category: :code_review, urgency: :low do
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/4796')
merge_request = find_project_merge_request(params[:merge_request_iid])
@@ -524,7 +524,7 @@ module API
params do
optional :skip_ci, type: Boolean, desc: 'Do not create CI pipeline'
end
- put ':id/merge_requests/:merge_request_iid/rebase', feature_category: :code_review do
+ put ':id/merge_requests/:merge_request_iid/rebase', feature_category: :code_review, urgency: :low do
merge_request = find_project_merge_request(params[:merge_request_iid])
authorize_push_to_merge_request!(merge_request)
@@ -543,7 +543,7 @@ module API
params do
use :pagination
end
- get ':id/merge_requests/:merge_request_iid/closes_issues', feature_category: :code_review do
+ get ':id/merge_requests/:merge_request_iid/closes_issues', feature_category: :code_review, urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
issues = ::Kaminari.paginate_array(merge_request.visible_closing_issues_for(current_user))
issues = paginate(issues)
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 656eaa2b2bb..7629f84cec2 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -7,6 +7,11 @@ module API
before { authenticate! }
+ urgency :low, [
+ '/projects/:id/merge_requests/:noteable_id/notes',
+ '/projects/:id/merge_requests/:noteable_id/notes/:note_id'
+ ]
+
Helpers::NotesHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb
index 33589f6c393..cd56809f45a 100644
--- a/lib/api/resource_label_events.rb
+++ b/lib/api/resource_label_events.rb
@@ -24,7 +24,7 @@ module API
use :pagination
end
- get ":id/#{eventables_str}/:eventable_id/resource_label_events", feature_category: feature_category do
+ get ":id/#{eventables_str}/:eventable_id/resource_label_events", feature_category: feature_category, urgency: :low do
eventable = find_noteable(eventable_type, params[:eventable_id])
events = eventable.resource_label_events.inc_relations
diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb
index c0483ca59c2..04d71faa56a 100644
--- a/lib/api/resource_milestone_events.rb
+++ b/lib/api/resource_milestone_events.rb
@@ -26,7 +26,7 @@ module API
use :pagination
end
- get ":id/#{eventables_str}/:eventable_id/resource_milestone_events", feature_category: feature_category do
+ get ":id/#{eventables_str}/:eventable_id/resource_milestone_events", feature_category: feature_category, urgency: :low do
eventable = find_noteable(eventable_type, params[:eventable_id])
events = ResourceMilestoneEventFinder.new(current_user, eventable).execute
diff --git a/lib/api/resource_state_events.rb b/lib/api/resource_state_events.rb
index 9b6f6a954b4..4b92f320d6f 100644
--- a/lib/api/resource_state_events.rb
+++ b/lib/api/resource_state_events.rb
@@ -25,7 +25,7 @@ module API
use :pagination
end
- get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category do
+ get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category, urgency: :low do
eventable = find_noteable(eventable_class, params[:eventable_iid])
events = ResourceStateEventFinder.new(current_user, eventable).execute
diff --git a/lib/api/suggestions.rb b/lib/api/suggestions.rb
index 7921700e365..0697169b49a 100644
--- a/lib/api/suggestions.rb
+++ b/lib/api/suggestions.rb
@@ -14,7 +14,7 @@ module API
requires :id, type: String, desc: 'The suggestion ID'
optional :commit_message, type: String, desc: "A custom commit message to use instead of the default generated message or the project's default message"
end
- put ':id/apply' do
+ put ':id/apply', urgency: :low do
suggestion = Suggestion.find_by_id(params[:id])
if suggestion
@@ -31,7 +31,7 @@ module API
requires :ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: "An array of suggestion ID's"
optional :commit_message, type: String, desc: "A custom commit message to use instead of the default generated message or the project's default message"
end
- put 'batch_apply' do
+ put 'batch_apply', urgency: :low do
ids = params[:ids]
suggestions = Suggestion.id_in(ids)
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index f47c3decd3c..fddcc1492a8 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -54,8 +54,7 @@ variables:
# KUBE_INGRESS_BASE_DOMAIN: domain.example.com
# Allows Container-Scanning to correctly correlate image names when using Jobs/Build.gitlab-ci.yml
- CI_APPLICATION_TAG: $CI_COMMIT_SHA
- CS_DEFAULT_BRANCH_IMAGE: $CI_REGISTRY_IMAGE/$CI_DEFAULT_BRANCH:$CI_APPLICATION_TAG
+ CS_DEFAULT_BRANCH_IMAGE: $CI_REGISTRY_IMAGE/$CI_DEFAULT_BRANCH:$CI_COMMIT_SHA
POSTGRES_USER: user
POSTGRES_PASSWORD: testing-password
diff --git a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
index 5266afe297f..8ce203d4585 100644
--- a/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
+++ b/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
@@ -67,16 +67,7 @@ module Gitlab
end
def get_wal_locations(job)
- job['dedup_wal_locations'] || job['wal_locations'] || legacy_wal_location(job)
- end
-
- # Already scheduled jobs could still contain legacy database write location.
- # TODO: remove this in the next iteration
- # https://gitlab.com/gitlab-org/gitlab/-/issues/338213
- def legacy_wal_location(job)
- wal_location = job['database_write_location'] || job['database_replica_location']
-
- { ::Gitlab::Database::MAIN_DATABASE_NAME.to_sym => wal_location } if wal_location
+ job['dedup_wal_locations'] || job['wal_locations']
end
def load_balancing_available?(worker_class)
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 9ad902efb3a..bb3ba1129fc 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -56,7 +56,6 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
push_frontend_feature_flag(:improved_emoji_picker, default_enabled: :yaml)
push_frontend_feature_flag(:new_header_search, default_enabled: :yaml)
- push_frontend_feature_flag(:configure_iac_scanning_via_mr, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
end
diff --git a/lib/gitlab/merge_requests/commit_message_generator.rb b/lib/gitlab/merge_requests/commit_message_generator.rb
index c420385b7c1..0e9ec6f5cb3 100644
--- a/lib/gitlab/merge_requests/commit_message_generator.rb
+++ b/lib/gitlab/merge_requests/commit_message_generator.rb
@@ -35,7 +35,9 @@ module Gitlab
"Closes #{closes_issues_references.to_sentence}"
end,
'description' => ->(merge_request) { merge_request.description.presence || '' },
- 'reference' => ->(merge_request) { merge_request.to_reference(full: true) }
+ 'reference' => ->(merge_request) { merge_request.to_reference(full: true) },
+ 'first_commit' => -> (merge_request) { merge_request.first_commit&.safe_message&.strip.presence || '' },
+ 'first_multiline_commit' => -> (merge_request) { merge_request.first_multiline_commit&.safe_message&.strip.presence || merge_request.title }
}.freeze
PLACEHOLDERS_REGEX = Regexp.union(PLACEHOLDERS.keys.map do |key|
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 79bd3329e20..6f4eeb23d3b 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -243,7 +243,9 @@ namespace :gitlab do
# Only for development environments,
# we execute pending data migrations inline for convenience.
Rake::Task['db:migrate'].enhance do
- Rake::Task['gitlab:db:execute_batched_migrations'].invoke if Rails.env.development?
+ if Rails.env.development? && Gitlab::Database::BackgroundMigration::BatchedMigration.table_exists?
+ Rake::Task['gitlab:db:execute_batched_migrations'].invoke
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d666d45d237..a4518e914ec 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2036,7 +2036,7 @@ msgstr ""
msgid "Add a task list"
msgstr ""
-msgid "Add a title…"
+msgid "Add a title..."
msgstr ""
msgid "Add a to do"
@@ -4769,6 +4769,9 @@ msgstr ""
msgid "Assign reviewer(s)"
msgstr ""
+msgid "Assign severity"
+msgstr ""
+
msgid "Assign some issues to this milestone."
msgstr ""
@@ -32840,6 +32843,9 @@ msgstr ""
msgid "Something went wrong while updating assignees"
msgstr ""
+msgid "Something went wrong while updating work item. Please try again"
+msgstr ""
+
msgid "Something went wrong while updating your list settings"
msgstr ""
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index 20936977612..e01c5522b51 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -79,14 +79,6 @@ RSpec.describe ApplicationExperiment, :experiment do
application_experiment.publish
end
- it "publishes to the database if we've opted for that" do
- application_experiment.record!
-
- expect(application_experiment).to receive(:publish_to_database)
-
- application_experiment.publish
- end
-
context 'when we should not track' do
let(:should_track) { false }
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index 56f1fa162bf..893865962d8 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
sequence(:username) { |n| "user#{n}" }
- sequence(:name) { |n| "John Doe#{n}" }
+ sequence(:name) { |n| "Sidney Jones#{n}" }
sequence(:email) { |n| "user#{n}@example.org" }
sequence(:email_alias) { |n| "user.alias#{n}@example.org" }
sequence(:title) { |n| "My title #{n}" }
diff --git a/spec/features/alert_management/alert_details_spec.rb b/spec/features/alert_management/alert_details_spec.rb
index ce82b5adf8d..579b8221041 100644
--- a/spec/features/alert_management/alert_details_spec.rb
+++ b/spec/features/alert_management/alert_details_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe 'Alert details', :js do
expect(alert_status).to have_content('Triggered')
- find('.btn-link').click
+ find('.gl-button').click
find('.gl-new-dropdown-item', text: 'Acknowledged').click
wait_for_requests
@@ -79,7 +79,7 @@ RSpec.describe 'Alert details', :js do
wait_for_requests
- expect(alert_assignee).to have_content('Assignee Edit John Doe')
+ expect(alert_assignee).to have_content('Assignee Edit Sidney Jones')
end
end
end
diff --git a/spec/features/gitlab_experiments_spec.rb b/spec/features/gitlab_experiments_spec.rb
index 76b418adcea..ca772680ff6 100644
--- a/spec/features/gitlab_experiments_spec.rb
+++ b/spec/features/gitlab_experiments_spec.rb
@@ -31,9 +31,10 @@ RSpec.describe "Gitlab::Experiment", :js do
expect(page).to have_content('Abuse Reports')
- published_experiments = page.evaluate_script('window.gon.experiment')
+ published_experiments = page.evaluate_script('window.gl.experiments')
expect(published_experiments).to include({
'null_hypothesis' => {
+ 'excluded' => false,
'experiment' => 'null_hypothesis',
'key' => anything,
'variant' => 'candidate'
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 0cefbae4d37..b0e4729db8b 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -180,7 +180,7 @@ RSpec.describe 'GFM autocomplete', :js do
describe 'assignees' do
it 'does not wrap with quotes for assignee values' do
- fill_in 'Comment', with: "@#{user.username[0]}"
+ fill_in 'Comment', with: "@#{user.username}"
find_highlighted_autocomplete_item.click
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 9df430c0f78..aae5ab58b5d 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Pipeline Schedules', :js do
click_link 'Take ownership'
page.within('.pipeline-schedule-table-row') do
expect(page).not_to have_content('No owner')
- expect(page).to have_link('John Doe')
+ expect(page).to have_link('Sidney Jones')
end
end
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 6cccdd2989f..fc736f2d155 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,11 +1,13 @@
+import * as Sentry from '@sentry/browser';
import createFlash, {
- createAction,
hideFlash,
- removeFlashClickListener,
+ addDismissFlashClickListener,
FLASH_TYPES,
FLASH_CLOSED_EVENT,
} from '~/flash';
+jest.mock('@sentry/browser');
+
describe('Flash', () => {
describe('hideFlash', () => {
let el;
@@ -66,49 +68,6 @@ describe('Flash', () => {
});
});
- describe('createAction', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- });
-
- it('creates link with href', () => {
- el.innerHTML = createAction({
- href: 'testing',
- title: 'test',
- });
-
- expect(el.querySelector('.flash-action').href).toContain('testing');
- });
-
- it('uses hash as href when no href is present', () => {
- el.innerHTML = createAction({
- title: 'test',
- });
-
- expect(el.querySelector('.flash-action').href).toContain('#');
- });
-
- it('adds role when no href is present', () => {
- el.innerHTML = createAction({
- title: 'test',
- });
-
- expect(el.querySelector('.flash-action').getAttribute('role')).toBe('button');
- });
-
- it('escapes the title text', () => {
- el.innerHTML = createAction({
- title: '<script>alert("a")</script>',
- });
-
- expect(el.querySelector('.flash-action').textContent.trim()).toBe(
- '<script>alert("a")</script>',
- );
- });
- });
-
describe('createFlash', () => {
const message = 'test';
const fadeTransition = false;
@@ -194,7 +153,26 @@ describe('Flash', () => {
expect(document.body.className).not.toContain('flash-shown');
});
+ it('does not capture error using Sentry', () => {
+ createFlash({ ...defaultParams, captureError: false, error: new Error('Error!') });
+
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('captures error using Sentry', () => {
+ createFlash({ ...defaultParams, captureError: true, error: new Error('Error!') });
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
+ expect(Sentry.captureException).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Error!',
+ }),
+ );
+ });
+
describe('with actionConfig', () => {
+ const findFlashAction = () => document.querySelector('.flash-container .flash-action');
+
it('adds action link', () => {
createFlash({
...defaultParams,
@@ -203,20 +181,69 @@ describe('Flash', () => {
},
});
- expect(document.querySelector('.flash-action')).not.toBeNull();
+ expect(findFlashAction()).not.toBeNull();
+ });
+
+ it('creates link with href', () => {
+ createFlash({
+ ...defaultParams,
+ actionConfig: {
+ href: 'testing',
+ title: 'test',
+ },
+ });
+
+ expect(findFlashAction().href).toBe(`${window.location}testing`);
+ expect(findFlashAction().textContent.trim()).toBe('test');
+ });
+
+ it('uses hash as href when no href is present', () => {
+ createFlash({
+ ...defaultParams,
+ actionConfig: {
+ title: 'test',
+ },
+ });
+
+ expect(findFlashAction().href).toBe(`${window.location}#`);
+ });
+
+ it('adds role when no href is present', () => {
+ createFlash({
+ ...defaultParams,
+ actionConfig: {
+ title: 'test',
+ },
+ });
+
+ expect(findFlashAction().getAttribute('role')).toBe('button');
+ });
+
+ it('escapes the title text', () => {
+ createFlash({
+ ...defaultParams,
+ actionConfig: {
+ title: '<script>alert("a")</script>',
+ },
+ });
+
+ expect(findFlashAction().textContent.trim()).toBe('<script>alert("a")</script>');
});
it('calls actionConfig clickHandler on click', () => {
- const actionConfig = {
- title: 'test',
- clickHandler: jest.fn(),
- };
+ const clickHandler = jest.fn();
- createFlash({ ...defaultParams, actionConfig });
+ createFlash({
+ ...defaultParams,
+ actionConfig: {
+ title: 'test',
+ clickHandler,
+ },
+ });
- document.querySelector('.flash-action').click();
+ findFlashAction().click();
- expect(actionConfig.clickHandler).toHaveBeenCalled();
+ expect(clickHandler).toHaveBeenCalled();
});
});
@@ -236,7 +263,7 @@ describe('Flash', () => {
});
});
- describe('removeFlashClickListener', () => {
+ describe('addDismissFlashClickListener', () => {
let el;
describe('with close icon', () => {
@@ -252,7 +279,7 @@ describe('Flash', () => {
});
it('removes global flash on click', (done) => {
- removeFlashClickListener(el, false);
+ addDismissFlashClickListener(el, false);
el.querySelector('.js-close-icon').click();
@@ -276,7 +303,7 @@ describe('Flash', () => {
});
it('does not throw', () => {
- expect(() => removeFlashClickListener(el, false)).not.toThrow();
+ expect(() => addDismissFlashClickListener(el, false)).not.toThrow();
});
});
});
diff --git a/spec/frontend/issue_spec.js b/spec/frontend/issues/issue_spec.js
index 952ef54d286..8a089b372ff 100644
--- a/spec/frontend/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -1,7 +1,7 @@
import { getByText } from '@testing-library/dom';
import MockAdapter from 'axios-mock-adapter';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import Issue from '~/issue';
+import Issue from '~/issues/issue';
import axios from '~/lib/utils/axios_utils';
describe('Issue', () => {
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index e619ab8cbfe..0300132308c 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -318,8 +318,14 @@ describe('Blob content viewer component', () => {
repository: { empty },
} = projectMock;
+ afterEach(() => {
+ delete gon.current_user_id;
+ delete gon.current_username;
+ });
+
it('renders component', async () => {
window.gon.current_user_id = 1;
+ window.gon.current_username = 'root';
await createComponent({ pushCode, downloadCode, empty }, mount);
@@ -330,28 +336,34 @@ describe('Blob content viewer component', () => {
deletePath: webPath,
canPushCode: pushCode,
canLock: true,
- isLocked: false,
+ isLocked: true,
emptyRepo: empty,
});
});
it.each`
- canPushCode | canDownloadCode | canLock
- ${true} | ${true} | ${true}
- ${false} | ${true} | ${false}
- ${true} | ${false} | ${false}
- `('passes the correct lock states', async ({ canPushCode, canDownloadCode, canLock }) => {
- await createComponent(
- {
- pushCode: canPushCode,
- downloadCode: canDownloadCode,
- empty,
- },
- mount,
- );
+ canPushCode | canDownloadCode | username | canLock
+ ${true} | ${true} | ${'root'} | ${true}
+ ${false} | ${true} | ${'root'} | ${false}
+ ${true} | ${false} | ${'root'} | ${false}
+ ${true} | ${true} | ${'peter'} | ${false}
+ `(
+ 'passes the correct lock states',
+ async ({ canPushCode, canDownloadCode, username, canLock }) => {
+ gon.current_username = username;
+
+ await createComponent(
+ {
+ pushCode: canPushCode,
+ downloadCode: canDownloadCode,
+ empty,
+ },
+ mount,
+ );
- expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
- });
+ expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
+ },
+ );
it('does not render if not logged in', async () => {
isLoggedIn.mockReturnValueOnce(false);
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index f67eed34a58..74d35daf578 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -47,7 +47,13 @@ export const projectMock = {
id: '1234',
userPermissions: userPermissionsMock,
pathLocks: {
- nodes: [],
+ nodes: [
+ {
+ id: 'test',
+ path: simpleViewerMock.path,
+ user: { id: '123', username: 'root' },
+ },
+ ],
},
repository: {
empty: false,
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
new file mode 100644
index 00000000000..0f6e7091c59
--- /dev/null
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -0,0 +1,56 @@
+import { shallowMount } from '@vue/test-utils';
+import { escape } from 'lodash';
+import ItemTitle from '~/work_items/components/item_title.vue';
+
+jest.mock('lodash/escape', () => jest.fn((fn) => fn));
+
+const createComponent = ({ initialTitle = 'Sample title', disabled = false } = {}) =>
+ shallowMount(ItemTitle, {
+ propsData: {
+ initialTitle,
+ disabled,
+ },
+ });
+
+describe('ItemTitle', () => {
+ let wrapper;
+ const mockUpdatedTitle = 'Updated title';
+ const findInputEl = () => wrapper.find('span#item-title');
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders title contents', () => {
+ expect(findInputEl().attributes()).toMatchObject({
+ 'data-placeholder': 'Add a title...',
+ contenteditable: 'true',
+ });
+ expect(findInputEl().text()).toBe('Sample title');
+ });
+
+ it('renders title contents with editing disabled', () => {
+ wrapper = createComponent({
+ disabled: true,
+ });
+
+ expect(wrapper.classes()).toContain('gl-cursor-not-allowed');
+ expect(findInputEl().attributes('contenteditable')).toBe('false');
+ });
+
+ it.each`
+ eventName | sourceEvent
+ ${'title-changed'} | ${'blur'}
+ ${'title-input'} | ${'keyup'}
+ `('emits "$eventName" event on input $sourceEvent', async ({ eventName, sourceEvent }) => {
+ findInputEl().element.innerText = mockUpdatedTitle;
+ await findInputEl().trigger(sourceEvent);
+
+ expect(wrapper.emitted(eventName)).toBeTruthy();
+ expect(escape).toHaveBeenCalledWith(mockUpdatedTitle);
+ });
+});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index efb4aa2feb2..c8d46b51888 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -15,3 +15,22 @@ export const workItemQueryResponse = {
},
},
};
+
+export const updateWorkItemMutationResponse = {
+ __typename: 'UpdateWorkItemPayload',
+ workItem: {
+ __typename: 'WorkItem',
+ id: '1',
+ widgets: {
+ __typename: 'WorkItemWidgetConnection',
+ nodes: [
+ {
+ __typename: 'TitleWidget',
+ type: 'TITLE',
+ enabled: true,
+ contentText: 'Updated title',
+ },
+ ],
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index 180f61f559f..71e153d30c3 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -5,6 +5,7 @@ import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
+import ItemTitle from '~/work_items/components/item_title.vue';
import { resolvers } from '~/work_items/graphql/resolvers';
Vue.use(VueApollo);
@@ -14,9 +15,9 @@ describe('Create work item component', () => {
let fakeApollo;
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findTitleInput = () => wrapper.findComponent(ItemTitle);
const findCreateButton = () => wrapper.find('[data-testid="create-button"]');
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
- const findTitleInput = () => wrapper.find('[data-testid="title-input"]');
const createComponent = ({ data = {} } = {}) => {
fakeApollo = createMockApollo([], resolvers);
@@ -70,9 +71,10 @@ describe('Create work item component', () => {
});
describe('when title input field has a text', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ const mockTitle = 'Test title';
createComponent();
- findTitleInput().setValue('Test title');
+ await findTitleInput().vm.$emit('title-input', mockTitle);
});
it('renders a non-disabled Create button', () => {
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index ea76e2628d3..02795751f33 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -2,8 +2,12 @@ import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
+import ItemTitle from '~/work_items/components/item_title.vue';
+import { resolvers } from '~/work_items/graphql/resolvers';
import { workItemQueryResponse } from '../mock_data';
Vue.use(VueApollo);
@@ -14,10 +18,10 @@ describe('Work items root component', () => {
let wrapper;
let fakeApollo;
- const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findTitle = () => wrapper.findComponent(ItemTitle);
const createComponent = ({ queryResponse = workItemQueryResponse } = {}) => {
- fakeApollo = createMockApollo();
+ fakeApollo = createMockApollo([], resolvers);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: workItemQuery,
variables: {
@@ -43,7 +47,28 @@ describe('Work items root component', () => {
createComponent();
expect(findTitle().exists()).toBe(true);
- expect(findTitle().text()).toBe('Test');
+ expect(findTitle().props('initialTitle')).toBe('Test');
+ });
+
+ it('updates the title when it is edited', async () => {
+ createComponent();
+ jest.spyOn(wrapper.vm.$apollo, 'mutate');
+ const mockUpdatedTitle = 'Updated title';
+
+ await findTitle().vm.$emit('title-changed', mockUpdatedTitle);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: WORK_ITEM_ID,
+ title: mockUpdatedTitle,
+ },
+ },
+ });
+
+ await waitForPromises();
+ expect(findTitle().props('initialTitle')).toBe(mockUpdatedTitle);
});
it('does not render the title if title is not in the widgets list', () => {
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 5a596526a7e..b481c214ca1 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -395,4 +395,170 @@ RSpec.describe AuthHelper do
end
end
end
+
+ describe '#auth_strategy_class' do
+ subject(:auth_strategy_class) { helper.auth_strategy_class(name) }
+
+ context 'when configuration specifies no provider' do
+ let(:name) { 'does_not_exist' }
+
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ end
+
+ it 'returns false' do
+ expect(auth_strategy_class).to be_falsey
+ end
+ end
+
+ context 'when configuration specifies a provider with args but without strategy_class' do
+ let(:name) { 'google_oauth2' }
+ let(:provider) do
+ Struct.new(:name, :args).new(
+ name,
+ 'app_id' => 'YOUR_APP_ID'
+ )
+ end
+
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
+ end
+
+ it 'returns false' do
+ expect(auth_strategy_class).to be_falsey
+ end
+ end
+
+ context 'when configuration specifies a provider with args and strategy_class' do
+ let(:name) { 'provider1' }
+ let(:strategy) { 'OmniAuth::Strategies::LDAP' }
+ let(:provider) do
+ Struct.new(:name, :args).new(
+ name,
+ 'strategy_class' => strategy
+ )
+ end
+
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
+ end
+
+ it 'returns the class' do
+ expect(auth_strategy_class).to eq(strategy)
+ end
+ end
+
+ context 'when configuration specifies another provider with args and another strategy_class' do
+ let(:name) { 'provider1' }
+ let(:strategy) { 'OmniAuth::Strategies::LDAP' }
+ let(:provider) do
+ Struct.new(:name, :args).new(
+ 'another_name',
+ 'strategy_class' => strategy
+ )
+ end
+
+ before do
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
+ end
+
+ it 'returns false' do
+ expect(auth_strategy_class).to be_falsey
+ end
+ end
+ end
+
+ describe '#saml_providers' do
+ subject(:saml_providers) { helper.saml_providers }
+
+ let(:saml_strategy) { 'OmniAuth::Strategies::SAML' }
+
+ let(:saml_provider_1_name) { 'saml_provider_1' }
+ let(:saml_provider_1) do
+ Struct.new(:name, :args).new(
+ saml_provider_1_name,
+ 'strategy_class' => saml_strategy
+ )
+ end
+
+ let(:saml_provider_2_name) { 'saml_provider_2' }
+ let(:saml_provider_2) do
+ Struct.new(:name, :args).new(
+ saml_provider_2_name,
+ 'strategy_class' => saml_strategy
+ )
+ end
+
+ let(:ldap_provider_name) { 'ldap_provider' }
+ let(:ldap_strategy) { 'OmniAuth::Strategies::LDAP' }
+ let(:ldap_provider) do
+ Struct.new(:name, :args).new(
+ ldap_provider_name,
+ 'strategy_class' => ldap_strategy
+ )
+ end
+
+ let(:google_oauth2_provider_name) { 'google_oauth2' }
+ let(:google_oauth2_provider) do
+ Struct.new(:name, :args).new(
+ google_oauth2_provider_name,
+ 'app_id' => 'YOUR_APP_ID'
+ )
+ end
+
+ context 'when configuration specifies no provider' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([])
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
+ end
+
+ it 'returns an empty list' do
+ expect(saml_providers).to be_empty
+ end
+ end
+
+ context 'when configuration specifies a provider with a SAML strategy_class' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name])
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1])
+ end
+
+ it 'returns the provider' do
+ expect(saml_providers).to match_array([saml_provider_1_name])
+ end
+ end
+
+ context 'when configuration specifies two providers with a SAML strategy_class' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name])
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2])
+ end
+
+ it 'returns the provider' do
+ expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ end
+ end
+
+ context 'when configuration specifies a provider with a non-SAML strategy_class' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name])
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider])
+ end
+
+ it 'returns an empty list' do
+ expect(saml_providers).to be_empty
+ end
+ end
+
+ context 'when configuration specifies four providers but only two with SAML strategy_class' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name])
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
+ end
+
+ it 'returns the provider' do
+ expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
index f04be91a057..b89f61b53f8 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
@@ -5,7 +5,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_gitlab_redis_queues do
let(:middleware) { described_class.new }
let(:worker) { worker_class.new }
- let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8' } }
+ let(:location) {'0/D525E3A8' }
+ let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } }
+ let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations } }
before do
skip_feature_flags_yaml_validation
@@ -60,9 +62,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
end
shared_examples_for 'replica is up to date' do |expected_strategy|
- let(:location) {'0/D525E3A8' }
- let(:wal_locations) { { Gitlab::Database::MAIN_DATABASE_NAME.to_sym => location } }
-
it 'does not stick to the primary', :aggregate_failures do
expect(ActiveRecord::Base.load_balancer)
.to receive(:select_up_to_date_host)
@@ -114,19 +113,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
it_behaves_like 'replica is up to date', 'replica'
end
- context 'when legacy wal location is set' do
- let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'database_write_location' => '0/D525E3A8' } }
-
- before do
- allow(ActiveRecord::Base.load_balancer)
- .to receive(:select_up_to_date_host)
- .with('0/D525E3A8')
- .and_return(true)
- end
-
- it_behaves_like 'replica is up to date', 'replica'
- end
-
context 'when database location is not set' do
let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e' } }
@@ -146,7 +132,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
end
context 'when WAL locations are present', :freeze_time do
- let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", "database_replica_location" => "0/D525E3A8", "created_at" => Time.current.to_f - elapsed_time } }
+ let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations, "created_at" => Time.current.to_f - elapsed_time } }
context 'when delay interval has not elapsed' do
let(:elapsed_time) { described_class::MINIMUM_DELAY_INTERVAL - 0.3 }
@@ -192,7 +178,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
include_examples 'stick to the primary', 'primary'
context 'when delay interval has not elapsed', :freeze_time do
- let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8', "created_at" => Time.current.to_f - elapsed_time } }
+ let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations, "created_at" => Time.current.to_f - elapsed_time } }
let(:elapsed_time) { described_class::MINIMUM_DELAY_INTERVAL - 0.3 }
it 'does not sleep' do
@@ -235,7 +221,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
end
context 'when job is retried' do
- let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8', 'retry_count' => 0 } }
+ let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'wal_locations' => wal_locations, 'retry_count' => 0 } }
context 'and replica still lagging behind' do
include_examples 'stick to the primary', 'primary'
diff --git a/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb b/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb
index 4de5c9b9c82..65c76aac10c 100644
--- a/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb
+++ b/spec/lib/gitlab/merge_requests/commit_message_generator_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
end
let(:user) { project.creator }
+ let(:source_branch) { 'feature' }
let(:merge_request_description) { "Merge Request Description\nNext line" }
let(:merge_request_title) { 'Bugfix' }
let(:merge_request) do
@@ -24,6 +25,8 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
:simple,
source_project: project,
target_project: project,
+ target_branch: 'master',
+ source_branch: source_branch,
author: user,
description: merge_request_description,
title: merge_request_title
@@ -226,6 +229,50 @@ RSpec.describe Gitlab::MergeRequests::CommitMessageGenerator do
MSG
end
end
+
+ context 'when project has merge commit template with first_commit' do
+ let(message_template_name) { <<~MSG.rstrip }
+ Message: %{first_commit}
+ MSG
+
+ it 'uses first commit' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Message: Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ MSG
+ end
+
+ context 'when branch has no unmerged commits' do
+ let(:source_branch) { 'v1.1.0' }
+
+ it 'is an empty string' do
+ expect(result_message).to eq 'Message: '
+ end
+ end
+ end
+
+ context 'when project has merge commit template with first_multiline_commit' do
+ let(message_template_name) { <<~MSG.rstrip }
+ Message: %{first_multiline_commit}
+ MSG
+
+ it 'uses first multiline commit' do
+ expect(result_message).to eq <<~MSG.rstrip
+ Message: Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ MSG
+ end
+
+ context 'when branch has no multiline commits' do
+ let(:source_branch) { 'spooky-stuff' }
+
+ it 'is mr title' do
+ expect(result_message).to eq 'Message: Bugfix'
+ end
+ end
+ end
end
end
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index f9df84e8ff4..3b521086c14 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -75,4 +75,37 @@ RSpec.describe Clusters::Agent do
expect(agent.has_access_to?(create(:project))).to be_falsey
end
end
+
+ describe '#active?' do
+ let_it_be(:agent) { create(:cluster_agent) }
+
+ let!(:token) { create(:cluster_agent_token, agent: agent, last_used_at: last_used_at) }
+
+ subject { agent.active? }
+
+ context 'agent has never connected' do
+ let(:last_used_at) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'agent has connected, but not recently' do
+ let(:last_used_at) { 2.hours.ago }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'agent has connected recently' do
+ let(:last_used_at) { 2.minutes.ago }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'agent has multiple tokens' do
+ let!(:inactive_token) { create(:cluster_agent_token, agent: agent, last_used_at: 2.hours.ago) }
+ let(:last_used_at) { 2.minutes.ago }
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index bde4798abec..ad9f948224f 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -39,7 +39,9 @@ RSpec.describe Clusters::AgentToken do
end
describe '#track_usage', :clean_gitlab_redis_cache do
- let(:agent_token) { create(:cluster_agent_token) }
+ let_it_be(:agent) { create(:cluster_agent) }
+
+ let(:agent_token) { create(:cluster_agent_token, agent: agent) }
subject { agent_token.track_usage }
@@ -73,6 +75,34 @@ RSpec.describe Clusters::AgentToken do
expect_redis_update
end
end
+
+ context 'agent is inactive' do
+ before do
+ allow(agent).to receive(:active?).and_return(false)
+ end
+
+ it 'creates an activity event' do
+ expect { subject }.to change { agent.activity_events.count }
+
+ event = agent.activity_events.last
+ expect(event).to have_attributes(
+ kind: 'agent_connected',
+ level: 'info',
+ recorded_at: agent_token.reload.read_attribute(:last_used_at),
+ agent_token: agent_token
+ )
+ end
+ end
+
+ context 'agent is active' do
+ before do
+ allow(agent).to receive(:active?).and_return(true)
+ end
+
+ it 'does not create an activity event' do
+ expect { subject }.not_to change { agent.activity_events.count }
+ end
+ end
end
def expect_redis_update
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d2b1114259e..253b7d65d33 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1648,6 +1648,9 @@ RSpec.describe MergeRequest, factory_default: :keep do
it 'uses template from target project' do
request = build(:merge_request, title: 'Fix everything')
+ request.compare_commits = [
+ double(safe_message: 'Commit message', gitaly_commit?: true, merge_commit?: false, description?: false)
+ ]
subject.target_project.merge_commit_template = '%{title}'
expect(request.default_merge_commit_message)
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index d30b2721a36..4d20d62b864 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe MergeRequests::ApprovalService do
describe '#execute' do
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request) }
+ let(:merge_request) { create(:merge_request, reviewers: [user]) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, user: user, project: project, target: merge_request) }
@@ -59,6 +59,14 @@ RSpec.describe MergeRequests::ApprovalService do
service.execute(merge_request)
end
+ it 'removes attention requested state' do
+ expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
+ .with(project: project, current_user: user, merge_request: merge_request, user: user)
+ .and_call_original
+
+ service.execute(merge_request)
+ end
+
context 'with remaining approvals' do
it 'fires an approval webhook' do
expect(service).to receive(:execute_hooks).with(merge_request, 'approved')
diff --git a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
new file mode 100644
index 00000000000..fe4ce0dab5e
--- /dev/null
+++ b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:assignee_user) { create(:user) }
+ let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
+ let(:reviewer) { merge_request.find_reviewer(user) }
+ let(:assignee) { merge_request.find_assignee(assignee_user) }
+ let(:project) { merge_request.project }
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request) }
+ let(:result) { service.execute }
+
+ before do
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ context 'invalid permissions' do
+ let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'updates reviewers and assignees' do
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ service.execute
+ reviewer.reload
+ assignee.reload
+
+ expect(reviewer.state).to eq 'reviewed'
+ expect(assignee.state).to eq 'reviewed'
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 86d972bc516..d36a2f75cfe 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -54,6 +54,10 @@ RSpec.describe MergeRequests::CloseService do
expect(todo.reload).to be_done
end
+ it 'removes attention requested state' do
+ expect(merge_request.find_assignee(user2).attention_requested?).to eq(false)
+ end
+
context 'when auto merge is enabled' do
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
index c43f5db6059..fa3b1614e21 100644
--- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb
+++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
@@ -87,6 +87,14 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do
expect(todo).to be_pending
end
+ it 'removes attention requested state' do
+ expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
+ .with(project: project, current_user: user, merge_request: merge_request, user: user)
+ .and_call_original
+
+ execute
+ end
+
it 'tracks users assigned event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_users_assigned_to_mr).once.with(users: [assignee])
diff --git a/spec/services/merge_requests/remove_attention_requested_service_spec.rb b/spec/services/merge_requests/remove_attention_requested_service_spec.rb
new file mode 100644
index 00000000000..875afc2dc7e
--- /dev/null
+++ b/spec/services/merge_requests/remove_attention_requested_service_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::RemoveAttentionRequestedService do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:assignee_user) { create(:user) }
+ let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) }
+ let(:reviewer) { merge_request.find_reviewer(user) }
+ let(:assignee) { merge_request.find_assignee(assignee_user) }
+ let(:project) { merge_request.project }
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) }
+ let(:result) { service.execute }
+
+ before do
+ project.add_developer(current_user)
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ context 'invalid permissions' do
+ let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, user: user) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'reviewer does not exist' do
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: create(:user)) }
+
+ it 'returns an error' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'reviewer exists' do
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates reviewers state' do
+ service.execute
+ reviewer.reload
+
+ expect(reviewer.state).to eq 'reviewed'
+ end
+ end
+
+ context 'assignee exists' do
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: assignee_user) }
+
+ before do
+ assignee.update!(state: :reviewed)
+ end
+
+ it 'returns success' do
+ expect(result[:status]).to eq :success
+ end
+
+ it 'updates assignees state' do
+ service.execute
+ assignee.reload
+
+ expect(assignee.state).to eq 'reviewed'
+ end
+ end
+
+ context 'assignee is the same as reviewer' do
+ let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) }
+ let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) }
+ let(:assignee) { merge_request.find_assignee(user) }
+
+ it 'updates reviewers and assignees state' do
+ service.execute
+ reviewer.reload
+ assignee.reload
+
+ expect(reviewer.state).to eq 'reviewed'
+ expect(assignee.state).to eq 'reviewed'
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
index e5ba7bcefae..63fa61b8097 100644
--- a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
+++ b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb
@@ -70,6 +70,14 @@ RSpec.describe MergeRequests::ToggleAttentionRequestedService do
service.execute
end
+
+ it 'removes attention requested state' do
+ expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
+ .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
+ .and_call_original
+
+ service.execute
+ end
end
context 'assignee exists' do
@@ -101,6 +109,14 @@ RSpec.describe MergeRequests::ToggleAttentionRequestedService do
service.execute
end
+
+ it 'removes attention requested state' do
+ expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new)
+ .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user)
+ .and_call_original
+
+ service.execute
+ end
end
context 'assignee is the same as reviewer' do
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
index 96df5a5f972..eec911f3b6f 100644
--- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
@@ -161,7 +161,7 @@ RSpec.shared_examples 'User views a wiki page' do
commit = wiki.commit
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
- expect(page).to have_content('by John Doe')
+ expect(page).to have_content('by Sidney Jones')
expect(page).to have_content('updated home')
expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions')
expect(page).to have_content('some link')
@@ -174,7 +174,7 @@ RSpec.shared_examples 'User views a wiki page' do
commit = wiki.commit('HEAD^')
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
- expect(page).to have_content('by John Doe')
+ expect(page).to have_content('by Sidney Jones')
expect(page).to have_content('updated home')
expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions')
expect(page).to have_content('some link')
@@ -188,7 +188,7 @@ RSpec.shared_examples 'User views a wiki page' do
commit = wiki.commit('HEAD^')
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
- expect(page).to have_content('by John Doe')
+ expect(page).to have_content('by Sidney Jones')
expect(page).to have_content('created page: home')
expect(page).to have_content('Showing 1 changed file with 4 additions and 0 deletions')
expect(page).to have_content('Look at this')