Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-08 12:13:04 +00:00
parent cb09086128
commit 886ecba0bd
111 changed files with 1260 additions and 323 deletions

View File

@ -1 +1 @@
2.7.4
2.7.5

View File

@ -1 +1 @@
3ef55853e9e161204464868390d97d1a1577042d
fe6bcc9ca347b59714c46adf65d100dd93abde52

View File

@ -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,
};

View File

@ -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;
}
}

View File

@ -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

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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() {

View File

@ -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';

View File

@ -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: {

View File

@ -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,

View File

@ -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);
});
}

View File

@ -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';

View File

@ -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';

View File

@ -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();

View File

@ -1,3 +1,3 @@
import initForm from 'ee_else_ce/pages/projects/issues/form';
import initForm from 'ee_else_ce/issues/form';
initForm();

View File

@ -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';

View File

@ -1,3 +1,3 @@
import initForm from 'ee_else_ce/pages/projects/issues/form';
import initForm from 'ee_else_ce/issues/form';
initForm();

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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>

View File

@ -11,6 +11,10 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
nodes {
id
path
user {
id
username
}
}
}
repository {

View File

@ -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) => ({

View File

@ -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"

View File

@ -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 }));
}

View File

@ -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

View File

@ -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"
>

View File

@ -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>

View File

@ -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,
};
},
},
};

View File

@ -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!
}

View File

@ -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
}
}
}
}
}
}

View File

@ -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"

View File

@ -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>

View File

@ -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.

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -6,7 +6,7 @@ class NewProjectReadmeContentExperiment < ApplicationExperiment # rubocop:disabl
def run_with(project, variant: nil)
@project = project
record!
publish_to_database
run(variant)
end

View File

@ -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.',

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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:

View File

@ -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).

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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 ""

View File

@ -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 }

View File

@ -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}" }

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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();
});
});
});

View File

@ -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', () => {

View File

@ -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;
expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
});
await createComponent(
{
pushCode: canPushCode,
downloadCode: canDownloadCode,
empty,
},
mount,
);
expect(findBlobButtonGroup().props('canLock')).toBe(canLock);
},
);
it('does not render if not logged in', async () => {
isLoggedIn.mockReturnValueOnce(false);

View File

@ -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,

View File

@ -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);
});
});

View File

@ -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',
},
],
},
},
};

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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

View File

@ -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'

Some files were not shown because too many files have changed in this diff Show More