Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-16 00:15:50 +00:00
parent 1c898dc5c1
commit e04431d29e
96 changed files with 1848 additions and 621 deletions

View File

@ -0,0 +1,55 @@
<script>
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
export default {
components: { InputCopyToggleVisibility },
props: {
token: {
type: String,
required: true,
},
inputId: {
type: String,
required: true,
},
inputLabel: {
type: String,
required: true,
},
copyButtonTitle: {
type: String,
required: true,
},
},
computed: {
formInputGroupProps() {
return { id: this.inputId };
},
},
};
</script>
<template>
<div class="row">
<div class="col-lg-12">
<hr />
</div>
<div class="col-lg-4">
<h4 class="gl-mt-0"><slot name="title"></slot></h4>
<slot name="description"></slot>
</div>
<div class="col-lg-8">
<input-copy-toggle-visibility
:label="inputLabel"
:label-for="inputId"
:form-input-group-props="formInputGroupProps"
:value="token"
:copy-button-title="copyButtonTitle"
>
<template #description>
<slot name="input-description"></slot>
</template>
</input-copy-toggle-visibility>
</div>
</div>
</template>

View File

@ -0,0 +1,111 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { pickBy } from 'lodash';
import { s__ } from '~/locale';
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '../constants';
import Token from './token.vue';
export default {
i18n: {
canNotAccessOtherData: s__('AccessTokens|It cannot be used to access any other data.'),
[FEED_TOKEN]: {
label: s__('AccessTokens|Feed token'),
copyButtonTitle: s__('AccessTokens|Copy feed token'),
description: s__(
'AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.',
),
inputDescription: s__(
'AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}.',
),
resetConfirmMessage: s__(
'AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.',
),
},
[INCOMING_EMAIL_TOKEN]: {
label: s__('AccessTokens|Incoming email token'),
copyButtonTitle: s__('AccessTokens|Copy incoming email token'),
description: s__(
'AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.',
),
inputDescription: s__(
'AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}.',
),
resetConfirmMessage: s__(
'AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.',
),
},
[STATIC_OBJECT_TOKEN]: {
label: s__('AccessTokens|Static object token'),
copyButtonTitle: s__('AccessTokens|Copy static object token'),
description: s__(
'AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.',
),
inputDescription: s__(
'AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}.',
),
resetConfirmMessage: s__('AccessTokens|Are you sure?'),
},
},
htmlAttributes: {
[FEED_TOKEN]: {
inputId: 'feed_token',
containerTestId: 'feed-token-container',
},
[INCOMING_EMAIL_TOKEN]: {
inputId: 'incoming_email_token',
containerTestId: 'incoming-email-token-container',
},
[STATIC_OBJECT_TOKEN]: {
inputId: 'static_object_token',
containerTestId: 'static-object-token-container',
},
},
components: { Token, GlSprintf, GlLink },
inject: ['tokenTypes'],
computed: {
enabledTokenTypes() {
return pickBy(this.tokenTypes, (tokenData, tokenType) => {
return (
tokenData?.enabled &&
this.$options.i18n[tokenType] &&
this.$options.htmlAttributes[tokenType]
);
});
},
},
};
</script>
<template>
<div>
<token
v-for="(tokenData, tokenType) in enabledTokenTypes"
:key="tokenType"
:token="tokenData.token"
:input-id="$options.htmlAttributes[tokenType].inputId"
:input-label="$options.i18n[tokenType].label"
:copy-button-title="$options.i18n[tokenType].copyButtonTitle"
:data-testid="$options.htmlAttributes[tokenType].containerTestId"
>
<template #title>{{ $options.i18n[tokenType].label }}</template>
<template #description>
<p>{{ $options.i18n[tokenType].description }}</p>
<p>{{ $options.i18n.canNotAccessOtherData }}</p>
</template>
<template #input-description>
<gl-sprintf :message="$options.i18n[tokenType].inputDescription">
<template #link="{ content }">
<gl-link
:href="tokenData.resetPath"
:data-confirm="$options.i18n[tokenType].resetConfirmMessage"
data-method="put"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</template>
</token>
</div>
</template>

View File

@ -0,0 +1,4 @@
// Token types
export const FEED_TOKEN = 'feedToken';
export const INCOMING_EMAIL_TOKEN = 'incomingEmailToken';
export const STATIC_OBJECT_TOKEN = 'staticObjectToken';

View File

@ -1,9 +1,13 @@
import Vue from 'vue';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { __ } from '~/locale';
import ExpiresAtField from './components/expires_at_field.vue';
import TokensApp from './components/tokens_app.vue';
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from './constants';
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
@ -81,3 +85,29 @@ export const initProjectsField = () => {
return null;
};
export const initTokensApp = () => {
const el = document.getElementById('js-tokens-app');
if (!el) return false;
const tokensData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.tokensData), {
deep: true,
});
const tokenTypes = {
[FEED_TOKEN]: tokensData[FEED_TOKEN],
[INCOMING_EMAIL_TOKEN]: tokensData[INCOMING_EMAIL_TOKEN],
[STATIC_OBJECT_TOKEN]: tokensData[STATIC_OBJECT_TOKEN],
};
return new Vue({
el,
provide: {
tokenTypes,
},
render(createElement) {
return createElement(TokensApp);
},
});
};

View File

@ -33,7 +33,7 @@ class GlEmoji extends HTMLElement {
this.dataset.unicodeVersion = unicodeVersion;
emojiUnicode = emojiInfo.e;
this.innerHTML = emojiInfo.e;
this.textContent = emojiInfo.e;
this.title = emojiInfo.d;
}

View File

@ -4,7 +4,8 @@ import { produce } from 'immer';
import { __, s__ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import createContact from './queries/create_contact.mutation.graphql';
import createContactMutation from './queries/create_contact.mutation.graphql';
import updateContactMutation from './queries/update_contact.mutation.graphql';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
export default {
@ -21,6 +22,11 @@ export default {
type: Boolean,
required: true,
},
contact: {
type: Object,
required: false,
default: () => {},
},
},
data() {
return {
@ -35,66 +41,111 @@ export default {
},
computed: {
invalid() {
return this.firstName === '' || this.lastName === '' || this.email === '';
const { firstName, lastName, email } = this;
return firstName.trim() === '' || lastName.trim() === '' || email.trim() === '';
},
editMode() {
return Boolean(this.contact);
},
title() {
return this.editMode ? this.$options.i18n.editTitle : this.$options.i18n.newTitle;
},
buttonLabel() {
return this.editMode
? this.$options.i18n.editButtonLabel
: this.$options.i18n.createButtonLabel;
},
mutation() {
return this.editMode ? updateContactMutation : createContactMutation;
},
variables() {
const { contact, firstName, lastName, phone, email, description, editMode, groupId } = this;
const variables = {
input: {
firstName,
lastName,
phone,
email,
description,
},
};
if (editMode) {
variables.input.id = contact.id;
} else {
variables.input.groupId = convertToGraphQLId(TYPE_GROUP, groupId);
}
return variables;
},
},
mounted() {
if (this.editMode) {
const { contact } = this;
this.firstName = contact.firstName || '';
this.lastName = contact.lastName || '';
this.phone = contact.phone || '';
this.email = contact.email || '';
this.description = contact.description || '';
}
},
methods: {
save() {
const { mutation, variables, updateCache, close } = this;
this.submitting = true;
return this.$apollo
.mutate({
mutation: createContact,
variables: {
input: {
groupId: convertToGraphQLId(TYPE_GROUP, this.groupId),
firstName: this.firstName,
lastName: this.lastName,
phone: this.phone,
email: this.email,
description: this.description,
},
},
update: this.updateCache,
mutation,
variables,
update: updateCache,
})
.then(({ data }) => {
if (data.customerRelationsContactCreate.errors.length === 0) this.close(true);
if (
data.customerRelationsContactCreate?.errors.length === 0 ||
data.customerRelationsContactUpdate?.errors.length === 0
) {
close(true);
}
this.submitting = false;
})
.catch(() => {
this.errorMessages = [__('Something went wrong. Please try again.')];
this.errorMessages = [this.$options.i18n.somethingWentWrong];
this.submitting = false;
});
},
close(success) {
this.$emit('close', success);
},
updateCache(store, { data: { customerRelationsContactCreate } }) {
if (customerRelationsContactCreate.errors.length > 0) {
this.errorMessages = customerRelationsContactCreate.errors;
updateCache(store, { data }) {
const mutationData =
data.customerRelationsContactCreate || data.customerRelationsContactUpdate;
if (mutationData?.errors.length > 0) {
this.errorMessages = mutationData.errors;
return;
}
const variables = {
groupFullPath: this.groupFullPath,
};
const sourceData = store.readQuery({
const queryArgs = {
query: getGroupContactsQuery,
variables,
});
variables: { groupFullPath: this.groupFullPath },
};
const data = produce(sourceData, (draftState) => {
const sourceData = store.readQuery(queryArgs);
queryArgs.data = produce(sourceData, (draftState) => {
draftState.group.contacts.nodes = [
...sourceData.group.contacts.nodes,
customerRelationsContactCreate.contact,
...sourceData.group.contacts.nodes.filter(({ id }) => id !== this.contact?.id),
mutationData.contact,
];
});
store.writeQuery({
query: getGroupContactsQuery,
variables,
data,
});
store.writeQuery(queryArgs);
},
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.content-wrapper');
@ -107,14 +158,17 @@ export default {
},
},
i18n: {
buttonLabel: s__('Crm|Create new contact'),
createButtonLabel: s__('Crm|Create new contact'),
editButtonLabel: __('Save changes'),
cancel: __('Cancel'),
firstName: s__('Crm|First name'),
lastName: s__('Crm|Last name'),
email: s__('Crm|Email'),
phone: s__('Crm|Phone number (optional)'),
description: s__('Crm|Description (optional)'),
title: s__('Crm|New Contact'),
newTitle: s__('Crm|New contact'),
editTitle: s__('Crm|Edit contact'),
somethingWentWrong: __('Something went wrong. Please try again.'),
},
};
</script>
@ -127,7 +181,7 @@ export default {
@close="close(false)"
>
<template #title>
<h4>{{ $options.i18n.title }}</h4>
<h3>{{ title }}</h3>
</template>
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
<ul class="gl-mb-0! gl-ml-5">
@ -160,9 +214,9 @@ export default {
variant="confirm"
:disabled="invalid"
:loading="submitting"
data-testid="create-new-contact-button"
data-testid="save-contact-button"
type="submit"
>{{ $options.i18n.buttonLabel }}</gl-button
>{{ buttonLabel }}</gl-button
>
</span>
</form>

View File

@ -2,9 +2,11 @@
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_CONTACT } from '~/graphql_shared/constants';
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
import NewContactForm from './new_contact_form.vue';
import ContactForm from './contact_form.vue';
export default {
components: {
@ -12,7 +14,7 @@ export default {
GlButton,
GlLoadingIcon,
GlTable,
NewContactForm,
ContactForm,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -47,11 +49,19 @@ export default {
return this.$apollo.queries.contacts.loading;
},
showNewForm() {
return this.$route.path.startsWith('/new');
return this.$route.name === NEW_ROUTE_NAME;
},
canCreateNew() {
showEditForm() {
return !this.isLoading && this.$route.name === EDIT_ROUTE_NAME;
},
canAdmin() {
return parseBoolean(this.canAdminCrmContact);
},
editingContact() {
return this.contacts.find(
(contact) => contact.id === convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id),
);
},
},
methods: {
extractContacts(data) {
@ -61,16 +71,28 @@ export default {
displayNewForm() {
if (this.showNewForm) return;
this.$router.push({ path: '/new' });
this.$router.push({ name: NEW_ROUTE_NAME });
},
hideNewForm(success) {
if (success) this.$toast.show(s__('Crm|Contact has been added'));
this.$router.replace({ path: '/' });
this.$router.replace({ name: INDEX_ROUTE_NAME });
},
hideEditForm(success) {
if (success) this.$toast.show(s__('Crm|Contact has been updated'));
this.editingContactId = 0;
this.$router.replace({ name: INDEX_ROUTE_NAME });
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
},
edit(value) {
if (this.showEditForm) return;
this.editingContactId = value;
this.$router.push({ name: EDIT_ROUTE_NAME, params: { id: value } });
},
},
fields: [
{ key: 'firstName', sortable: true },
@ -87,7 +109,7 @@ export default {
},
{
key: 'id',
label: __('Issues'),
label: '',
formatter: (id) => {
return getIdFromGraphQLId(id);
},
@ -96,6 +118,7 @@ export default {
i18n: {
emptyText: s__('Crm|No contacts found'),
issuesButtonLabel: __('View issues'),
editButtonLabel: __('Edit'),
title: s__('Crm|Customer Relations Contacts'),
newContact: s__('Crm|New contact'),
errorText: __('Something went wrong. Please try again.'),
@ -116,7 +139,7 @@ export default {
</h2>
<div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end">
<gl-button
v-if="canCreateNew"
v-if="canAdmin"
variant="confirm"
data-testid="new-contact-button"
@click="displayNewForm"
@ -125,7 +148,13 @@ export default {
</gl-button>
</div>
</div>
<new-contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<contact-form
v-if="showEditForm"
:contact="editingContact"
:drawer-open="showEditForm"
@close="hideEditForm"
/>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
@ -138,11 +167,20 @@ export default {
<template #cell(id)="data">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
class="gl-mr-3"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="getIssuesPath(groupIssuesPath, data.value)"
/>
<gl-button
v-if="canAdmin"
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
data-testid="edit-contact-button"
icon="pencil"
:aria-label="$options.i18n.editButtonLabel"
@click="edit(data.value)"
/>
</template>
</gl-table>
</div>

View File

@ -0,0 +1,10 @@
#import "./crm_contact_fields.fragment.graphql"
mutation updateContact($input: CustomerRelationsContactUpdateInput!) {
customerRelationsContactUpdate(input: $input) {
contact {
...ContactFragment
}
errors
}
}

View File

@ -0,0 +1,3 @@
export const INDEX_ROUTE_NAME = 'index';
export const NEW_ROUTE_NAME = 'new';
export const EDIT_ROUTE_NAME = 'edit';

View File

@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import CrmContactsRoot from './components/contacts_root.vue';
import routes from './routes';
Vue.use(VueApollo);
Vue.use(VueRouter);
@ -25,14 +26,7 @@ export default () => {
const router = new VueRouter({
base: basePath,
mode: 'history',
routes: [
{
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Contacts List',
path: '/',
component: CrmContactsRoot,
},
],
routes,
});
return new Vue({

View File

@ -0,0 +1,20 @@
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from './constants';
import CrmContactsRoot from './components/contacts_root.vue';
export default [
{
name: INDEX_ROUTE_NAME,
path: '/',
component: CrmContactsRoot,
},
{
name: NEW_ROUTE_NAME,
path: '/new',
component: CrmContactsRoot,
},
{
name: EDIT_ROUTE_NAME,
path: '/:id/edit',
component: CrmContactsRoot,
},
];

View File

@ -16,3 +16,6 @@ export const CATEGORY_ICON_MAP = {
export const EMOJIS_PER_ROW = 9;
export const EMOJI_ROW_HEIGHT = 34;
export const CATEGORY_ROW_HEIGHT = 37;
export const CACHE_VERSION_KEY = 'gl-emoji-map-version';
export const CACHE_KEY = 'gl-emoji-map';

View File

@ -1,9 +1,9 @@
import { escape, minBy } from 'lodash';
import emojiRegexFactory from 'emoji-regex';
import emojiAliases from 'emojis/aliases.json';
import { sanitize } from '~/lib/dompurify';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
let emojiMap = null;
let validEmojiNames = null;
@ -17,10 +17,15 @@ const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
if (
isLocalStorageAvailable &&
window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION &&
window.localStorage.getItem('gl-emoji-map')
window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
window.localStorage.getItem(CACHE_KEY)
) {
return JSON.parse(window.localStorage.getItem('gl-emoji-map'));
const emojis = JSON.parse(window.localStorage.getItem(CACHE_KEY));
// Workaround because the pride flag is broken in EMOJI_VERSION = '1'
if (emojis.gay_pride_flag) {
emojis.gay_pride_flag.e = '🏳️‍🌈';
}
return emojis;
}
// We load the JSON file direct from the server
@ -29,15 +34,19 @@ async function loadEmoji() {
const { data } = await axios.get(
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
);
window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-map', JSON.stringify(data));
window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
return data;
}
async function loadEmojiWithNames() {
return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
acc[key] = { ...value, name: key, e: sanitize(value.e) };
const emojiRegex = emojiRegexFactory();
return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
// Filter out entries which aren't emojis
if (value.e.match(emojiRegex)?.[0] === value.e) {
acc[key] = { ...value, name: key };
}
return acc;
}, {});
}

View File

@ -1,6 +1,8 @@
export const MINIMUM_SEARCH_LENGTH = 3;
export const TYPE_CI_RUNNER = 'Ci::Runner';
export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_EPIC = 'Epic';
export const TYPE_GROUP = 'Group';
export const TYPE_ISSUE = 'Issue';
@ -8,11 +10,10 @@ export const TYPE_ITERATION = 'Iteration';
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
export const TYPE_MERGE_REQUEST = 'MergeRequest';
export const TYPE_MILESTONE = 'Milestone';
export const TYPE_NOTE = 'Note';
export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';
export const TYPE_PROJECT = 'Project';
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability';
export const TYPE_NOTE = 'Note';
export const TYPE_DISCUSSION = 'Discussion';
export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';

View File

@ -1,4 +1,5 @@
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
import { initExpiresAtField, initProjectsField, initTokensApp } from '~/access_tokens';
initExpiresAtField();
initProjectsField();
initTokensApp();

View File

@ -23,9 +23,9 @@ module AuthenticatesWithTwoFactor
session[:otp_user_id] = user.id
session[:user_password_hash] = Digest::SHA256.hexdigest(user.encrypted_password)
push_frontend_feature_flag(:webauthn)
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
if Feature.enabled?(:webauthn)
if Feature.enabled?(:webauthn, default_enabled: :yaml)
setup_webauthn_authentication(user)
else
setup_u2f_authentication(user)

View File

@ -11,7 +11,7 @@ module AuthenticatesWithTwoFactorForAdminMode
return handle_locked_user(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
push_frontend_feature_flag(:webauthn)
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
if user.two_factor_webauthn_enabled?
setup_webauthn_authentication(user)

View File

@ -9,6 +9,10 @@ class Groups::Crm::ContactsController < Groups::ApplicationController
render action: "index"
end
def edit
render action: "index"
end
private
def authorize_read_crm_contact!

View File

@ -8,7 +8,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
helper_method :current_password_required?
before_action do
push_frontend_feature_flag(:webauthn)
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
end
feature_category :authentication_and_authorization
@ -44,7 +44,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
@qr_code = build_qr_code
@account_string = account_string
if Feature.enabled?(:webauthn)
if Feature.enabled?(:webauthn, default_enabled: :yaml)
setup_webauthn_registration
else
setup_u2f_registration
@ -69,7 +69,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
@error = { message: _('Invalid pin code.') }
@qr_code = build_qr_code
if Feature.enabled?(:webauthn)
if Feature.enabled?(:webauthn, default_enabled: :yaml)
setup_webauthn_registration
else
setup_u2f_registration

View File

@ -8,7 +8,7 @@ class ProfilesController < Profiles::ApplicationController
before_action :authorize_change_username!, only: :update_username
skip_before_action :require_email, only: [:show, :update]
before_action do
push_frontend_feature_flag(:webauthn)
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
end
feature_category :users

View File

@ -32,7 +32,7 @@ class SessionsController < Devise::SessionsController
before_action :load_recaptcha
before_action :set_invite_params, only: [:new]
before_action do
push_frontend_feature_flag(:webauthn)
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
end
after_action :log_failed_login, if: :action_new_and_failed_login?
@ -305,9 +305,9 @@ class SessionsController < Devise::SessionsController
def authentication_method
if user_params[:otp_attempt]
AuthenticationEvent::TWO_FACTOR
elsif user_params[:device_response] && Feature.enabled?(:webauthn)
elsif user_params[:device_response] && Feature.enabled?(:webauthn, default_enabled: :yaml)
AuthenticationEvent::TWO_FACTOR_WEBAUTHN
elsif user_params[:device_response] && !Feature.enabled?(:webauthn)
elsif user_params[:device_response] && !Feature.enabled?(:webauthn, default_enabled: :yaml)
AuthenticationEvent::TWO_FACTOR_U2F
else
AuthenticationEvent::STANDARD

View File

@ -1,7 +1,30 @@
# frozen_string_literal: true
module AccessTokensHelper
include AccountsHelper
include ApplicationHelper
def scope_description(prefix)
prefix == :project_access_token ? [:doorkeeper, :project_access_token_scope_desc] : [:doorkeeper, :scope_desc]
end
def tokens_app_data
{
feed_token: {
enabled: !Gitlab::CurrentSettings.disable_feed_token,
token: current_user.feed_token,
reset_path: reset_feed_token_profile_path
},
incoming_email_token: {
enabled: incoming_email_token_enabled?,
token: current_user.enabled_incoming_email_token,
reset_path: reset_incoming_email_token_profile_path
},
static_object_token: {
enabled: static_objects_external_storage_enabled?,
token: current_user.enabled_static_object_token,
reset_path: reset_static_object_token_profile_path
}
}.to_json
end
end

View File

@ -13,7 +13,7 @@ class MembersPreloader
ActiveRecord::Associations::Preloader.new.preload(members, :created_by)
ActiveRecord::Associations::Preloader.new.preload(members, user: :status)
ActiveRecord::Associations::Preloader.new.preload(members, user: :u2f_registrations)
ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn)
ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn, default_enabled: :yaml)
end
end

View File

@ -105,27 +105,32 @@ module Namespaces
:traversal_ids,
'LEAD (namespaces.traversal_ids, 1) OVER (ORDER BY namespaces.traversal_ids ASC) next_traversal_ids'
)
cte = Gitlab::SQL::CTE.new(:base_cte, base)
base_cte = Gitlab::SQL::CTE.new(:base_cte, base)
namespaces = Arel::Table.new(:namespaces)
records = unscoped
.with(cte.to_arel)
.from([cte.table, namespaces])
# Bound the search space to ourselves (optional) and descendants.
#
# WHERE (base_cte.next_traversal_ids IS NULL OR base_cte.next_traversal_ids > namespaces.traversal_ids)
# AND next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids
records = records
.where(cte.table[:next_traversal_ids].eq(nil).or(cte.table[:next_traversal_ids].gt(namespaces[:traversal_ids])))
.where(next_sibling_func(cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
records = unscoped
.from([base_cte.table, namespaces])
.where(base_cte.table[:next_traversal_ids].eq(nil).or(base_cte.table[:next_traversal_ids].gt(namespaces[:traversal_ids])))
.where(next_sibling_func(base_cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
# AND base_cte.traversal_ids <= namespaces.traversal_ids
if include_self
records.where(cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
else
records.where(cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
end
records = if include_self
records.where(base_cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
else
records.where(base_cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
end
records_cte = Gitlab::SQL::CTE.new(:descendants_cte, records)
unscoped
.unscope(where: [:type])
.with(base_cte.to_arel, records_cte.to_arel)
.from(records_cte.alias_to(namespaces))
end
def next_sibling_func(*args)

View File

@ -187,8 +187,8 @@ class User < ApplicationRecord
has_one :abuse_report, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent
has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' # rubocop:disable Cop/ActiveRecordDependent
has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' # rubocop:disable Cop/ActiveRecordDependent
has_many :builds, class_name: 'Ci::Build'
has_many :pipelines, class_name: 'Ci::Pipeline'
has_many :todos
has_many :notification_settings
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@ -911,7 +911,7 @@ class User < ApplicationRecord
end
def two_factor_u2f_enabled?
return false if Feature.enabled?(:webauthn)
return false if Feature.enabled?(:webauthn, default_enabled: :yaml)
if u2f_registrations.loaded?
u2f_registrations.any?
@ -925,7 +925,7 @@ class User < ApplicationRecord
end
def two_factor_webauthn_enabled?
return false unless Feature.enabled?(:webauthn)
return false unless Feature.enabled?(:webauthn, default_enabled: :yaml)
(webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
end
@ -1790,7 +1790,7 @@ class User < ApplicationRecord
# we do this on read since migrating all existing users is not a feasible
# solution.
def feed_token
Gitlab::CurrentSettings.disable_feed_token ? nil : ensure_feed_token!
ensure_feed_token! unless Gitlab::CurrentSettings.disable_feed_token
end
# Each existing user needs to have a `static_object_token`.
@ -1800,6 +1800,14 @@ class User < ApplicationRecord
ensure_static_object_token!
end
def enabled_static_object_token
static_object_token if Gitlab::CurrentSettings.static_objects_external_storage_enabled?
end
def enabled_incoming_email_token
incoming_email_token if Gitlab::IncomingEmail.supports_issue_creation?
end
def sync_attribute?(attribute)
return true if ldap_user? && attribute == :email

View File

@ -63,6 +63,12 @@ class MemberEntity < Grape::Entity
member.respond_to?(:invited_user_state) ? member.invited_user_state : ""
end
end
private
def current_user
options[:current_user]
end
end
MemberEntity.prepend_mod_with('MemberEntity')

View File

@ -32,62 +32,64 @@
type_plural: type_plural,
active_tokens: @active_personal_access_tokens,
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
- if Feature.enabled?(:hide_access_tokens)
#js-tokens-app{ data: { tokens_data: tokens_app_data } }
- else
- unless Gitlab::CurrentSettings.disable_feed_token
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Feed token')
%p
= s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
= reset_message.html_safe
- unless Gitlab::CurrentSettings.disable_feed_token
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Feed token')
%p
= s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
= reset_message.html_safe
- if incoming_email_token_enabled?
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Incoming email token')
%p
= s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
= reset_message.html_safe
- if incoming_email_token_enabled?
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= s_('AccessTokens|Incoming email token')
%p
= s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
= reset_message.html_safe
- if static_objects_external_storage_enabled?
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4
%h4.gl-mt-0
= s_('AccessTokens|Static object token')
%p
= s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8
= label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
= text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
%p.form-text.text-muted
- reset_link = url_for [:reset, :static_object_token, :profile]
- reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
- reset_link_end = '</a>'.html_safe
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
= reset_message.html_safe
- if static_objects_external_storage_enabled?
.col-lg-12
%hr
.row.gl-mt-3.js-search-settings-section
.col-lg-4
%h4.gl-mt-0
= s_('AccessTokens|Static object token')
%p
= s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8
= label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
= text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
%p.form-text.text-muted
- reset_link = url_for [:reset, :static_object_token, :profile]
- reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
- reset_link_end = '</a>'.html_safe
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
= reset_message.html_safe

View File

@ -2,7 +2,7 @@
- page_title _('Two-Factor Authentication'), _('Account')
- add_to_breadcrumbs _('Account'), profile_account_path
- @content_class = "limit-container-width" unless fluid_layout
- webauthn_enabled = Feature.enabled?(:webauthn)
- webauthn_enabled = Feature.enabled?(:webauthn, default_enabled: :yaml)
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
.row.gl-mt-3

View File

@ -13,7 +13,7 @@
%span.cgray= starrer.user.to_reference
- if starrer.user == current_user
%span.badge-pill.badge-success.gl-badge.gl-ml-2.sm= _("It's you")
= gl_badge_tag _("It's you"), variant: :success, size: :sm, class: 'gl-ml-2'
.block-truncated
= time_ago_with_tooltip(starrer.starred_since)

View File

@ -3,14 +3,10 @@
require_relative '../metrics_server/metrics_server'
begin
target = ENV['METRICS_SERVER_TARGET']
raise "Required: METRICS_SERVER_TARGET=[sidekiq]" unless target == 'sidekiq'
target = ENV['METRICS_SERVER_TARGET']
raise "METRICS_SERVER_TARGET cannot be blank" if target.blank?
metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}")
wipe_metrics_dir = Gitlab::Utils.to_boolean(ENV['WIPE_METRICS_DIR']) || false
metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}")
wipe_metrics_dir = Gitlab::Utils.to_boolean(ENV['WIPE_METRICS_DIR']) || false
# Re-raise exceptions in threads on the main thread.
Thread.abort_on_exception = true
MetricsServer.new(target, metrics_dir, wipe_metrics_dir).start
end
Process.wait(MetricsServer.spawn(target, metrics_dir: metrics_dir, wipe_metrics_dir: wipe_metrics_dir))

View File

@ -1,8 +1,8 @@
---
name: geo_pages_deployment_verification
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74905
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346754
name: hide_access_tokens
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76280
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347490
milestone: '14.6'
type: development
group: group::geo
group: group::access
default_enabled: false

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232671
milestone: '13.4'
type: development
group: group::access
default_enabled: false
default_enabled: true

View File

@ -127,7 +127,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
namespace :crm do
resources :contacts, only: [:index, :new]
resources :contacts, only: [:index, :new, :edit]
resources :organizations, only: [:index]
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class ChangePackageIndexOnCorpus < Gitlab::Database::Migration[1.0]
INDEX_NAME = 'index_coverage_fuzzing_corpuses_on_package_id'
disable_ddl_transaction!
# Changing this index is safe.
# The table does not have any data in it as it's behind a feature flag.
def up
remove_concurrent_index :coverage_fuzzing_corpuses, :package_id, name: INDEX_NAME
add_concurrent_index :coverage_fuzzing_corpuses, :package_id, unique: true, name: INDEX_NAME
end
def down
remove_concurrent_index :coverage_fuzzing_corpuses, :package_id, name: INDEX_NAME
add_concurrent_index :coverage_fuzzing_corpuses, :package_id, name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
8960c0a2b7e621e466fde3bde6a252119008579c058046a16d57a6f6bff42008

View File

@ -25870,7 +25870,7 @@ CREATE INDEX index_container_repository_on_name_trigram ON container_repositorie
CREATE UNIQUE INDEX index_content_blocked_states_on_container_id_commit_sha_path ON content_blocked_states USING btree (container_identifier, commit_sha, path);
CREATE INDEX index_coverage_fuzzing_corpuses_on_package_id ON coverage_fuzzing_corpuses USING btree (package_id);
CREATE UNIQUE INDEX index_coverage_fuzzing_corpuses_on_package_id ON coverage_fuzzing_corpuses USING btree (package_id);
CREATE INDEX index_coverage_fuzzing_corpuses_on_project_id ON coverage_fuzzing_corpuses USING btree (project_id);

View File

@ -54,8 +54,8 @@ verification methods:
| Blobs | External Merge Request Diffs _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | External Merge Request Diffs _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | Pipeline artifacts _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | Pipeline artifacts _(object storage)_ | Geo with API/Managed (*2*) | SHA256 checksum |
| Blobs | Pages _(file system)_ | Geo with API | _Not implemented_ |
| Blobs | Pipeline artifacts _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | Pages _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | Pages _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
- (*1*): Redis replication can be used as part of HA with Redis sentinel. It's not used between Geo sites.
@ -147,7 +147,6 @@ these epics/issues:
- [Geo: Improve the self-service Geo replication framework](https://gitlab.com/groups/gitlab-org/-/epics/3761)
- [Geo: Move existing blobs to framework](https://gitlab.com/groups/gitlab-org/-/epics/3588)
- [Geo: Add unreplicated data types](https://gitlab.com/groups/gitlab-org/-/epics/893)
- [Geo: Support GitLab Pages](https://gitlab.com/groups/gitlab-org/-/epics/589)
### Replicated data types behind a feature flag
@ -190,7 +189,7 @@ successfully, you must replicate their data using some other means.
|[Project wiki repository](../../../user/project/wiki/) | **Yes** (10.2) | **Yes** (10.7) | No | |
|[Group wiki repository](../../../user/project/wiki/group.md) | [**Yes** (13.10)](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | No | No | Behind feature flag `geo_group_wiki_repository_replication`, enabled by default. |
|[Uploads](../../uploads.md) | **Yes** (10.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | No | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. |
|[LFS objects](../../lfs/index.md) | **Yes** (10.2) | **Yes**(14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification is under development behind the feature flag `geo_lfs_object_verification` introduced in 14.6. |
|[LFS objects](../../lfs/index.md) | **Yes** (10.2) | **Yes**(14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification is behind the feature flag `geo_lfs_object_verification` enabled by default in 14.6. |
|[Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|[Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|[CI job artifacts](../../../ci/pipelines/job_artifacts.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. Job logs also verified on transfer. |
@ -203,7 +202,7 @@ successfully, you must replicate their data using some other means.
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.12) | Via Object Storage provider if supported. Native Geo support (Beta). | Replication is behind the feature flag `geo_terraform_state_version_replication`, enabled by default. Verification was behind the feature flag `geo_terraform_state_version_verification`, which was removed in 14.0. |
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification is behind the feature flag `geo_merge_request_diff_verification`, enabled by default in 14.6.|
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | No | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_pages_deployment_replication`, enabled by default. |
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification is behind the feature flag `geo_pages_deployment_verification`, enabled by default in 14.6. |
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|[Elasticsearch integration](../../../integration/elasticsearch.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | Not planned because further product discovery is required and Elasticsearch (ES) clusters can be rebuilt. Secondaries use the same ES cluster as the primary. |
|[Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/259694) | No | No | Blocked by [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Replication of this cache is not needed for disaster recovery purposes because it can be recreated from external sources. |

View File

@ -275,24 +275,24 @@ GET /groups/:id/projects
Parameters:
| Attribute | Type | Required | Description |
| ----------------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user |
| `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, `similarity` (1), or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` |
| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
| `min_access_level` | integer | no | Limit to projects where current user has at least this [access level](members.md#valid-access-levels) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
| `with_security_reports` | boolean | no | **(ULTIMATE)** Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
| Attribute | Type | Required | Description |
| -------------------------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user |
| `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, `similarity` (1), or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` |
| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
| `min_access_level` | integer | no | Limit to projects where current user has at least this [access level](members.md#valid-access-levels) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
| `with_security_reports` **(ULTIMATE)** | boolean | no | Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
1. Order by similarity: Orders the results by a similarity score calculated from the provided `search`
URL parameter. When using `order_by=similarity`, the `sort` parameter is ignored. When the `search`
@ -783,28 +783,28 @@ POST /groups
Parameters:
| Attribute | Type | Required | Description |
| ------------------------------------ | ------- | -------- | ----------- |
| `name` | string | yes | The name of the group. |
| `path` | string | yes | The path of the group. |
| `description` | string | no | The group's description. |
| `membership_lock` | boolean | no | **(PREMIUM)** Prevent adding new members to projects within this group. |
| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. |
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
| `emails_disabled` | boolean | no | Disable email notifications |
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `parent_id` | integer | no | The parent group ID for creating nested group. |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
| `shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
| Attribute | Type | Required | Description |
| ------------------------------------------------------- | ------- | -------- | ----------- |
| `name` | string | yes | The name of the group. |
| `path` | string | yes | The path of the group. |
| `description` | string | no | The group's description. |
| `membership_lock` **(PREMIUM)** | boolean | no | Prevent adding new members to projects within this group. |
| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. |
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
| `emails_disabled` | boolean | no | Disable email notifications |
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `parent_id` | integer | no | The parent group ID for creating nested group. |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
| `shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
### Options for `default_branch_protection`
@ -884,32 +884,32 @@ Updates the project group. Only available to group owners and administrators.
PUT /groups/:id
```
| Attribute | Type | Required | Description |
| ------------------------------------------ | ------- | -------- | ----------- |
| `id` | integer | yes | The ID of the group. |
| `name` | string | no | The name of the group. |
| `path` | string | no | The path of the group. |
| `description` | string | no | The description of the group. |
| `membership_lock` | boolean | no | **(PREMIUM)** Prevent adding new members to projects within this group. |
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
| `emails_disabled` | boolean | no | Disable email notifications |
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). |
| `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. |
| `shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
| `prevent_forking_outside_group` | boolean | no | **(PREMIUM)** When enabled, users can **not** fork projects from this group to external namespaces
| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
| `prevent_sharing_groups_outside_hierarchy` | boolean | no | See [Prevent group sharing outside the group hierarchy](../user/group/index.md#prevent-group-sharing-outside-the-group-hierarchy). This attribute is only available on top-level groups. [Introduced in GitLab 14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/333721) |
| Attribute | Type | Required | Description |
| ------------------------------------------------------- | ------- | -------- | ----------- |
| `id` | integer | yes | The ID of the group. |
| `name` | string | no | The name of the group. |
| `path` | string | no | The path of the group. |
| `description` | string | no | The description of the group. |
| `membership_lock` **(PREMIUM)** | boolean | no | Prevent adding new members to projects within this group. |
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
| `emails_disabled` | boolean | no | Disable email notifications |
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
| `request_access_enabled` | boolean | no | Allow users to request member access. |
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). |
| `file_template_project_id` **(PREMIUM)** | integer | no | The ID of a project to load custom file templates from. |
| `shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
| `prevent_forking_outside_group` **(PREMIUM)** | boolean | no | When enabled, users can **not** fork projects from this group to external namespaces
| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
| `prevent_sharing_groups_outside_hierarchy` | boolean | no | See [Prevent group sharing outside the group hierarchy](../user/group/index.md#prevent-group-sharing-outside-the-group-hierarchy). This attribute is only available on top-level groups. [Introduced in GitLab 14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/333721) |
NOTE:
The `projects` and `shared_projects` attributes in the response are deprecated and [scheduled for removal in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797).

View File

@ -197,18 +197,18 @@ POST /projects/:id/protected_branches
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40"
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the branch or wildcard |
| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) |
| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) |
| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) |
| `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) |
| `allowed_to_push` | array | no | **(PREMIUM)** Array of access levels allowed to push, with each described by a hash |
| `allowed_to_merge` | array | no | **(PREMIUM)** Array of access levels allowed to merge, with each described by a hash |
| `allowed_to_unprotect` | array | no | **(PREMIUM)** Array of access levels allowed to unprotect, with each described by a hash |
| `code_owner_approval_required` | boolean | no | **(PREMIUM)** Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) |
| Attribute | Type | Required | Description |
| -------------------------------------------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the branch or wildcard |
| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) |
| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) |
| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) |
| `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) |
| `allowed_to_push` **(PREMIUM)** | array | no | Array of access levels allowed to push, with each described by a hash |
| `allowed_to_merge` **(PREMIUM)** | array | no | Array of access levels allowed to merge, with each described by a hash |
| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of access levels allowed to unprotect, with each described by a hash |
| `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) |
Example response:
@ -414,8 +414,8 @@ PATCH /projects/:id/protected_branches/:name
curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch"
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the branch |
| `code_owner_approval_required` | boolean | no | **(PREMIUM)** Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false)|
| Attribute | Type | Required | Description |
| -------------------------------------------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the branch |
| `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false)|

View File

@ -228,7 +228,7 @@ listed in the descriptions of the relevant settings.
| `after_sign_up_text` | string | no | Text shown to the user after signing up. |
| `akismet_api_key` | string | required by: `akismet_enabled` | API key for Akismet spam protection. |
| `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable Akismet spam protection. |
| `allow_group_owners_to_manage_ldap` | boolean | no | **(PREMIUM)** Set to `true` to allow group owners to manage LDAP. |
| `allow_group_owners_to_manage_ldap` **(PREMIUM)** | boolean | no | Set to `true` to allow group owners to manage LDAP. |
| `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from hooks and services. |
| `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. |
| `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from web hooks and services. |
@ -242,7 +242,7 @@ listed in the descriptions of the relevant settings.
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It automatically builds, tests, and deploys applications based on a predefined CI/CD configuration. |
| `automatic_purchased_storage_allocation` | boolean | no | Enabling this permits automatic allocation of purchased storage in a namespace. |
| `check_namespace_plan` | boolean | no | **(PREMIUM)** Enabling this makes only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |
| `check_namespace_plan` **(PREMIUM)** | boolean | no | Enabling this makes only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |
| `commit_email_hostname` | string | no | Custom hostname (for private commit emails). |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. |
| `deactivate_dormant_users` | boolean | no | Enable [automatic deactivation of dormant users](../user/admin_area/moderate_users.md#automatically-deactivate-dormant-users). |
@ -255,8 +255,8 @@ listed in the descriptions of the relevant settings.
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `delayed_project_deletion` | boolean | no | **(PREMIUM SELF)** Enable delayed project deletion by default in new groups. Default is `false`. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM SELF)** The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
| `delayed_project_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed project deletion by default in new groups. Default is `false`. |
| `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
| `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. |
| `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). |
| `diff_max_lines` | integer | no | Maximum [lines in a diff](../user/admin_area/diff_limits.md). |
@ -273,23 +273,23 @@ listed in the descriptions of the relevant settings.
| `eks_account_id` | string | no | Amazon account ID. |
| `eks_integration_enabled` | boolean | no | Enable integration with Amazon EKS. |
| `eks_secret_access_key` | string | no | AWS IAM secret access key. |
| `elasticsearch_aws_access_key` | string | no | **(PREMIUM)** AWS IAM access key. |
| `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the Elasticsearch domain is configured. |
| `elasticsearch_aws_secret_access_key` | string | no | **(PREMIUM)** AWS IAM secret access key. |
| `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch. |
| `elasticsearch_indexed_field_length_limit` | integer | no | **(PREMIUM)** Maximum size of text fields to index by Elasticsearch. 0 value means no limit. This does not apply to repository and wiki indexing. |
| `elasticsearch_indexed_file_size_limit_kb` | integer | no | **(PREMIUM)** Maximum size of repository and wiki files that are indexed by Elasticsearch. |
| `elasticsearch_indexing` | boolean | no | **(PREMIUM)** Enable Elasticsearch indexing. |
| `elasticsearch_limit_indexing` | boolean | no | **(PREMIUM)** Limit Elasticsearch to index certain namespaces and projects. |
| `elasticsearch_max_bulk_concurrency` | integer | no | **(PREMIUM)** Maximum concurrency of Elasticsearch bulk requests per indexing operation. This only applies to repository indexing operations. |
| `elasticsearch_max_bulk_size_mb` | integer | no | **(PREMIUM)** Maximum size of Elasticsearch bulk indexing requests in MB. This only applies to repository indexing operations. |
| `elasticsearch_namespace_ids` | array of integers | no | **(PREMIUM)** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
| `elasticsearch_project_ids` | array of integers | no | **(PREMIUM)** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
| `elasticsearch_search` | boolean | no | **(PREMIUM)** Enable Elasticsearch search. |
| `elasticsearch_url` | string | no | **(PREMIUM)** The URL to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (for example, `http://localhost:9200, http://localhost:9201"`). |
| `elasticsearch_username` | string | no | **(PREMIUM)** The `username` of your Elasticsearch instance. |
| `elasticsearch_password` | string | no | **(PREMIUM)** The password of your Elasticsearch instance. |
| `email_additional_text` | string | no | **(PREMIUM)** Additional text added to the bottom of every email for legal/auditing/compliance reasons. |
| `elasticsearch_aws_access_key` **(PREMIUM)** | string | no | AWS IAM access key. |
| `elasticsearch_aws_region` **(PREMIUM)** | string | no | The AWS region the Elasticsearch domain is configured. |
| `elasticsearch_aws_secret_access_key` **(PREMIUM)** | string | no | AWS IAM secret access key. |
| `elasticsearch_aws` **(PREMIUM)** | boolean | no | Enable the use of AWS hosted Elasticsearch. |
| `elasticsearch_indexed_field_length_limit` **(PREMIUM)** | integer | no | Maximum size of text fields to index by Elasticsearch. 0 value means no limit. This does not apply to repository and wiki indexing. |
| `elasticsearch_indexed_file_size_limit_kb` **(PREMIUM)** | integer | no | Maximum size of repository and wiki files that are indexed by Elasticsearch. |
| `elasticsearch_indexing` **(PREMIUM)** | boolean | no | Enable Elasticsearch indexing. |
| `elasticsearch_limit_indexing` **(PREMIUM)** | boolean | no | Limit Elasticsearch to index certain namespaces and projects. |
| `elasticsearch_max_bulk_concurrency` **(PREMIUM)** | integer | no | Maximum concurrency of Elasticsearch bulk requests per indexing operation. This only applies to repository indexing operations. |
| `elasticsearch_max_bulk_size_mb` **(PREMIUM)** | integer | no | Maximum size of Elasticsearch bulk indexing requests in MB. This only applies to repository indexing operations. |
| `elasticsearch_namespace_ids` **(PREMIUM)** | array of integers | no | The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
| `elasticsearch_project_ids` **(PREMIUM)** | array of integers | no | The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
| `elasticsearch_search` **(PREMIUM)** | boolean | no | Enable Elasticsearch search. |
| `elasticsearch_url` **(PREMIUM)** | string | no | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (for example, `http://localhost:9200, http://localhost:9201"`). |
| `elasticsearch_username` **(PREMIUM)** | string | no | The `username` of your Elasticsearch instance. |
| `elasticsearch_password` **(PREMIUM)** | string | no | The password of your Elasticsearch instance. |
| `email_additional_text` **(PREMIUM)** | string | no | Additional text added to the bottom of every email for legal/auditing/compliance reasons. |
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `enforce_namespace_storage_limit` | boolean | no | Enabling this permits enforcement of namespace storage limits. |
@ -304,11 +304,11 @@ listed in the descriptions of the relevant settings.
| `external_pipeline_validation_service_url` | string | no | URL to use for pipeline validation requests. |
| `external_pipeline_validation_service_token` | string | no | Optional. Token to include as the `X-Gitlab-Token` header in requests to the URL in `external_pipeline_validation_service_url`. |
| `external_pipeline_validation_service_timeout` | integer | no | How long to wait for a response from the pipeline validation service. Assumes `OK` if it times out. |
| `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. |
| `file_template_project_id` **(PREMIUM)** | integer | no | The ID of a project to load custom file templates from. |
| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. |
| `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
| `geo_status_timeout` | integer | no | **(PREMIUM)** The amount of seconds after which a request to get a secondary node status times out. |
| `git_two_factor_session_expiry` | integer | no | **(PREMIUM)** Maximum duration (in minutes) of a session for Git operations when 2FA is enabled. |
| `geo_node_allowed_ips` **(PREMIUM)** | string | yes | Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
| `geo_status_timeout` **(PREMIUM)** | integer | no | The amount of seconds after which a request to get a secondary node status times out. |
| `git_two_factor_session_expiry` **(PREMIUM)** | integer | no | Maximum duration (in minutes) of a session for Git operations when 2FA is enabled. |
| `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for Git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. |
| `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. |
| `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. |
@ -319,7 +319,7 @@ listed in the descriptions of the relevant settings.
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. |
| `help_page_support_url` | string | no | Alternate support URL for help page and help dropdown. |
| `help_page_text` | string | no | Custom text displayed on the help page. |
| `help_text` | string | no | **(PREMIUM)** GitLab server administrator information. |
| `help_text` **(PREMIUM)** | string | no | GitLab server administrator information. |
| `hide_third_party_offers` | boolean | no | Do not display offers from third parties in GitLab. |
| `home_page_url` | string | no | Redirect to this URL when not logged in. |
| `housekeeping_bitmaps_enabled` | boolean | required by: `housekeeping_enabled` | Enable Git pack file bitmap creation. |
@ -336,21 +336,21 @@ listed in the descriptions of the relevant settings.
| `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. |
| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook. |
| `mailgun_events_enabled` | boolean | no | Enable Mailgun event receiver. |
| `maintenance_mode_message` | string | no | **(PREMIUM)** Message displayed when instance is in maintenance mode. |
| `maintenance_mode` | boolean | no | **(PREMIUM)** When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. |
| `maintenance_mode_message` **(PREMIUM)** | string | no | Message displayed when instance is in maintenance mode. |
| `maintenance_mode` **(PREMIUM)** | boolean | no | When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. |
| `max_artifacts_size` | integer | no | Maximum artifacts size in MB. |
| `max_attachment_size` | integer | no | Limit attachment size in MB. |
| `max_import_size` | integer | no | Maximum import size in MB. 0 for unlimited. Default = 0 (unlimited) [Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8. |
| `max_pages_size` | integer | no | Maximum size of pages repositories in MB. |
| `max_personal_access_token_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for personal access tokens in days. |
| `max_ssh_key_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for SSH keys in days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6. |
| `max_personal_access_token_lifetime` **(ULTIMATE SELF)** | integer | no | Maximum allowable lifetime for personal access tokens in days. |
| `max_ssh_key_lifetime` **(ULTIMATE SELF)** | integer | no | Maximum allowable lifetime for SSH keys in days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6. |
| `metrics_method_call_threshold` | integer | no | A method call is only tracked when it takes longer than the given amount of milliseconds. |
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Administrators can configure repository mirroring. |
| `mirror_capacity_threshold` | integer | no | **(PREMIUM)** Minimum capacity to be available before scheduling more mirrors preemptively. |
| `mirror_max_capacity` | integer | no | **(PREMIUM)** Maximum number of mirrors that can be synchronizing at the same time. |
| `mirror_max_delay` | integer | no | **(PREMIUM)** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
| `npm_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use npmjs.org as a default remote repository when the package is not found in the GitLab Package Registry for npm. |
| `pypi_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use pypi.org as a default remote repository when the package is not found in the GitLab Package Registry for PyPI. |
| `mirror_capacity_threshold` **(PREMIUM)** | integer | no | Minimum capacity to be available before scheduling more mirrors preemptively. |
| `mirror_max_capacity` **(PREMIUM)** | integer | no | Maximum number of mirrors that can be synchronizing at the same time. |
| `mirror_max_delay` **(PREMIUM)** | integer | no | Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
| `npm_package_requests_forwarding` **(PREMIUM)** | boolean | no | Use npmjs.org as a default remote repository when the package is not found in the GitLab Package Registry for npm. |
| `pypi_package_requests_forwarding` **(PREMIUM)** | boolean | no | Use pypi.org as a default remote repository when the package is not found in the GitLab Package Registry for PyPI. |
| `outbound_local_requests_whitelist` | array of strings | no | Define a list of trusted domains or IP addresses to which local requests are allowed when local requests for hooks and services are disabled.
| `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. |
| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |
@ -365,7 +365,7 @@ listed in the descriptions of the relevant settings.
| `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. |
| `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** When enabled, GitLab runs a background job that produces pseudonymized CSVs of the GitLab database to upload to your configured object storage directory.
| `pseudonymizer_enabled` **(PREMIUM)** | boolean | no | When enabled, GitLab runs a background job that produces pseudonymized CSVs of the GitLab database to upload to your configured object storage directory.
| `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events are created. [Bulk push events are created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. |
| `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services fire or not. Webhooks and services aren't submitted if it surpasses that value. |
| `rate_limiting_response_text` | string | no | When rate limiting is enabled via the `throttle_*` settings, send this plain text response when a rate limit is exceeded. 'Retry later' is sent if this is blank. |
@ -375,7 +375,7 @@ listed in the descriptions of the relevant settings.
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. |
| `receive_max_input_size` | integer | no | Maximum push size (MB). |
| `repository_checks_enabled` | boolean | no | GitLab periodically runs `git fsck` in all project and wiki repositories to look for silent disk corruption issues. |
| `repository_size_limit` | integer | no | **(PREMIUM)** Size limit per repository (MB) |
| `repository_size_limit` **(PREMIUM)** | integer | no | Size limit per repository (MB) |
| `repository_storages_weighted` | hash of strings to integers | no | (GitLab 13.1 and later) Hash of names of taken from `gitlab.yml` to [weights](../administration/repository_storage_paths.md#configure-where-new-repositories-are-stored). New projects are created in one of these stores, chosen by a weighted random selection. |
| `repository_storages` | array of strings | no | (GitLab 13.0 and earlier) List of names of enabled storage paths, taken from `gitlab.yml`. New projects are created in one of these stores, chosen at random. |
| `require_admin_approval_after_user_signup` | boolean | no | When enabled, any user that signs up for an account using the registration form is placed under a **Pending approval** state and has to be explicitly [approved](../user/admin_area/moderate_users.md) by an administrator. |
@ -385,7 +385,7 @@ listed in the descriptions of the relevant settings.
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. |
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
| `shared_runners_minutes` | integer | required by: `shared_runners_enabled` | **(PREMIUM)** Set the maximum number of pipeline minutes that a group can use on shared runners per month. |
| `shared_runners_minutes` **(PREMIUM)** | integer | required by: `shared_runners_enabled` | Set the maximum number of pipeline minutes that a group can use on shared runners per month. |
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
| `sidekiq_job_limiter_mode` | string | no | `track` or `compress`. Sets the behavior for [Sidekiq job size limits](../user/admin_area/settings/sidekiq_job_limits.md). Default: 'compress'. |
| `sidekiq_job_limiter_compression_threshold_bytes` | integer | no | The threshold in bytes at which Sidekiq jobs are compressed before being stored in Redis. Default: 100 000 bytes (100KB). |
@ -393,10 +393,10 @@ listed in the descriptions of the relevant settings.
| `sign_in_text` | string | no | Text on the login page. |
| `signin_enabled` | string | no | (Deprecated: Use `password_authentication_enabled_for_web` instead) Flag indicating if password authentication is enabled for the web interface. |
| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
| `slack_app_enabled` | boolean | no | **(PREMIUM)** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. |
| `slack_app_id` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app ID of the Slack-app. |
| `slack_app_secret` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app secret of the Slack-app. |
| `slack_app_verification_token` | string | required by: `slack_app_enabled` | **(PREMIUM)** The verification token of the Slack-app. |
| `slack_app_enabled` **(PREMIUM)** | boolean | no | (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. |
| `slack_app_id` **(PREMIUM)** | string | required by: `slack_app_enabled` | The app ID of the Slack-app. |
| `slack_app_secret` **(PREMIUM)** | string | required by: `slack_app_enabled` | The app secret of the Slack-app. |
| `slack_app_verification_token` **(PREMIUM)** | string | required by: `slack_app_enabled` | The verification token of the Slack-app. |
| `snippet_size_limit` | integer | no | Max snippet content size in **bytes**. Default: 52428800 Bytes (50MB).|
| `snowplow_app_id` | string | no | The Snowplow site name / application ID. (for example, `gitlab`) |
| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (for example, `snowplow.trx.gitlab.net`) |

View File

@ -1,9 +1,6 @@
---
type: reference, dev
stage: none
group: Development
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
description: "Writing styles, markup, formatting, and other standards for the GitLab RESTful APIs."
info: For assistance with this Style Guide page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-other-projects-and-subjects.
description: 'Writing styles, markup, formatting, and other standards for the GitLab RESTful APIs.'
---
# RESTful API
@ -30,6 +27,10 @@ In the Markdown doc for a resource (AKA endpoint):
- Every method must have a detailed [description of the parameters](#method-description).
- Every method must have a cURL example.
- Every method must have a response body (in JSON format).
- If an attribute is available only to higher level tiers than the other
parameters, add the appropriate inline [tier badge](styleguide/index.md#product-tier-badges).
Put the badge in the **Attribute** column, like the
`**(<tier>)**` code in the following template.
## API topic template
@ -49,12 +50,12 @@ METHOD /endpoint
Supported attributes:
| Attribute | Type | Required | Description |
| :---------- | :------- | :--------------------- | :-------------------- |
| `attribute` | datatype | **{check-circle}** Yes | Detailed description. |
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
| Attribute | Type | Required | Description |
|:-------------------------|:---------|:-----------------------|:----------------------|
| `attribute` | datatype | **{check-circle}** Yes | Detailed description. |
| `attribute` **(<tier>)** | datatype | **{dotted-circle}** No | Detailed description. |
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
Example request:
@ -83,20 +84,20 @@ always be in code blocks using backticks (`` ` ``).
Sort the attributes in the table: first, required, then alphabetically.
```markdown
| Attribute | Type | Required | Description |
| :------------- | :------------ | :--------------------- | :--------------------------------------------------- |
| `user` | string | **{check-circle}** Yes | The GitLab username. |
| `assignee_ids` | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
| Attribute | Type | Required | Description |
|:-----------------------------|:--------------|:-----------------------|:-----------------------------------------------------|
| `user` | string | **{check-circle}** Yes | The GitLab username. |
| `assignee_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
```
Rendered example:
| Attribute | Type | Required | Description |
| :------------- | :------------ | :--------------------- | :--------------------------------------------------- |
| `user` | string | **{check-circle}** Yes | The GitLab username. |
| `assignee_ids` | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
| Attribute | Type | Required | Description |
|:-----------------------------|:--------------|:-----------------------|:-----------------------------------------------------|
| `user` | string | **{check-circle}** Yes | The GitLab username. |
| `assignee_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
## cURL commands
@ -109,7 +110,7 @@ Rendered example:
username and password.
| Methods | Description |
| :---------------------------------------------- | :----------------------------------------------------- |
|:------------------------------------------------|:-------------------------------------------------------|
| `--header "PRIVATE-TOKEN: <your_access_token>"` | Use this method as is, whenever authentication needed. |
| `--request POST` | Use this method when creating new objects |
| `--request PUT` | Use this method when updating existing objects |

View File

@ -1806,7 +1806,9 @@ after the heading text. For example:
# Heading title **(FREE)**
```
Do not add tier badges inline with other text. The single source of truth for a feature should be the heading where the functionality is described.
Do not add tier badges inline with other text, except for [API attributes](../restful_api_styleguide.md).
The single source of truth for a feature should be the heading where the
functionality is described.
#### Available product tier badges

View File

@ -107,19 +107,19 @@ The following options are available:
| Push rule | Description |
|---------------------------------|-------------|
| Removal of tags with `git push` | Forbid users to remove Git tags with `git push`. Tags can be deleted through the web UI. |
| Check whether the commit author is a GitLab user | Restrict commits to existing GitLab users (checked against their emails). |
| Reject unverified users | GitLab rejects any commit that was not committed by an authenticated user. |
| Check whether the commit author is a GitLab user | Restrict commits to existing GitLab users (checked against their emails). <sup>1</sup> |
| Reject unverified users | GitLab rejects any commit that was not committed by the same user as the user who pushed it, or where the committer's email address is not [confirmed](../security/user_email_confirmation.md). |
| Check whether commit is signed through GPG | Reject commit when it is not signed through GPG. Read [signing commits with GPG](../user/project/repository/gpg_signed_commits/index.md). |
| Prevent pushing secret files | GitLab rejects any files that are likely to contain secrets. See the [forbidden file names](#prevent-pushing-secrets-to-the-repository). |
| Require expression in commit messages | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
| Reject expression in commit messages | Only commit messages that do not match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
| Restrict by branch name | Only branch names that match this regular expression are allowed to be pushed. Leave empty to allow all branch names. |
| Restrict by commit author's email | Only commit author's email that match this regular expression are allowed to be pushed. Leave empty to allow any email. |
| Prohibited file names | Any committed filenames that match this regular expression and do not already exist in the repository are not allowed to be pushed. Leave empty to allow any filenames. See [common examples](#prohibited-file-names). |
| Require expression in commit messages | Only commit messages that match this regular expression are allowed to be pushed. <sup>2</sup> Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
| Reject expression in commit messages | Only commit messages that do not match this regular expression are allowed to be pushed. <sup>2</sup> Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
| Restrict by branch name | Only branch names that match this regular expression are allowed to be pushed. <sup>2</sup> Leave empty to allow all branch names. |
| Restrict by commit author's email | Only commit author's email that match this regular expression are allowed to be pushed. <sup>1</sup> <sup>2</sup> Leave empty to allow any email. |
| Prohibited file names | Any committed filenames that match this regular expression and do not already exist in the repository are not allowed to be pushed. <sup>2</sup> Leave empty to allow any filenames. See [common examples](#prohibited-file-names). |
| Maximum file size | Pushes that contain added or updated files that exceed this file size (in MB) are rejected. Set to 0 to allow files of any size. Files tracked by Git LFS are exempted. |
NOTE:
GitLab uses [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for regular expressions in push rules, and you can test them at the [regex101 regex tester](https://regex101.com/).
1. Checks both the commit author and committer.
1. GitLab uses [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for regular expressions in push rules, and you can test them at the [regex101 regex tester](https://regex101.com/).
### Caveat to "Reject unsigned commits" push rule

View File

@ -283,6 +283,30 @@ It also displays the following important statistics:
| Maximum users | The highest number of billable users on your system during the term of the loaded license. |
| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that needs to be paid for at renewal. |
## Export your license usage
> Introduced in GitLab 14.6.
If you are an administrator, you can export your license usage into a CSV:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Subscription**.
1. In the top right, select **Export license usage file**.
This file contains all the information GitLab needs to manually process quarterly reconciliations or renewals. If your instance is firewalled or air-gapped, you can provide GitLab with this information.
The **License Usage** CSV includes the following details:
- License key
- Email
- License start date
- License end date
- Company
- Generated at (the timestamp for when the file was exported)
- Table of historical user counts for each day in the period:
- Date the count was recorded
- Active user count
## Renew your subscription
To renew your subscription,

View File

@ -75,21 +75,21 @@ If you choose a size larger than the configured value for the web server,
you may receive errors. See the [troubleshooting section](#troubleshooting) for more
details.
## Personal Access Token prefix
## Personal access token prefix
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/342327) in GitLab 14.5. Default prefix added.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20968) in GitLab 13.7.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342327) in GitLab 14.5, a default prefix.
You can set a global prefix for all generated Personal Access Tokens.
You can specify a prefix for personal access tokens. You might use a prefix
to find tokens more quickly, or for use with automation tools.
A prefix can help you identify PATs visually, as well as with automation tools.
The default prefix is `glpat-` but administrators can change it.
NOTE:
For GitLab.com and self-managed instances, the default prefix is `glpat-`.
[Project access tokens](../../project/settings/project_access_tokens.md) also inherit this prefix.
### Set a prefix
Only a GitLab administrator can set the prefix, which is a global setting applied
to any PAT generated in the system by any user:
To change the default global prefix:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
@ -97,8 +97,8 @@ to any PAT generated in the system by any user:
1. Fill in the **Personal Access Token prefix** field.
1. Click **Save changes**.
It is also possible to configure the prefix via the [settings API](../../../api/settings.md)
using the `personal_access_token_prefix` field.
You can also configure the prefix by using the
[settings API](../../../api/settings.md).
## Repository size limit **(PREMIUM SELF)**

View File

@ -168,6 +168,36 @@ container_scanning:
CS_DISABLE_DEPENDENCY_LIST: "true"
```
#### Report language-specific findings
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/7277) in GitLab 14.6.
The `CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` CI/CD variable controls whether the scan reports
findings related to programming languages. The languages supported depend on the
[scanner used](#change-scanners):
- [Trivy](https://aquasecurity.github.io/trivy/latest/vulnerability/detection/language/).
- [Grype](https://github.com/anchore/grype#features).
By default, the report only includes packages managed by the Operating System (OS) package manager
(for example, `yum`, `apt`, `apk`, `tdnf`). To report security findings in non-OS packages, set
`CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` to `"false"`:
```yaml
include:
- template: Security/Container-Scanning.gitlab-ci.yml
container_scanning:
variables:
CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN: "false"
```
When you enable this feature, you may see [duplicate findings](../terminology/#duplicate-finding)
in the [Vulnerability Report](../vulnerability_report/)
if [Dependency Scanning](../dependency_scanning/)
is enabled for your project. This happens because GitLab can't automatically deduplicate the
findings reported by the two different analyzers.
#### Available CI/CD variables
You can [configure](#customizing-the-container-scanning-settings) analyzers by using the following CI/CD variables:

View File

@ -119,7 +119,7 @@ The following table lists project permissions available for each role:
| [Merge requests](project/merge_requests/index.md):<br>Apply code change suggestions | | | ✓ | ✓ | ✓ |
| [Merge requests](project/merge_requests/index.md):<br>Approve (*9*) | | | ✓ | ✓ | ✓ |
| [Merge requests](project/merge_requests/index.md):<br>Assign | | | ✓ | ✓ | ✓ |
| [Merge requests](project/merge_requests/index.md):<br>Create | | | ✓ | ✓ | ✓ |
| [Merge requests](project/merge_requests/index.md):<br>Create (*18*) | | | ✓ | ✓ | ✓ |
| [Merge requests](project/merge_requests/index.md):<br>Add labels | | | ✓ | ✓ | ✓ |
| [Merge requests](project/merge_requests/index.md):<br>Lock threads | | | ✓ | ✓ | ✓ |
| [Merge requests](project/merge_requests/index.md):<br>Manage or accept | | | ✓ | ✓ | ✓ |
@ -233,6 +233,7 @@ The following table lists project permissions available for each role:
1. Guest users can only set metadata (for example, labels, assignees, or milestones)
when creating an issue. They cannot change the metadata on existing issues.
1. In GitLab 14.5 or later, Guests are not allowed to [create incidents](../operations/incident_management/incidents.md#incident-creation).
1. In projects that accept contributions from external members, users can create, edit, and close their own merge requests.
## Project features permissions

View File

@ -20,8 +20,7 @@ password secret.
NOTE:
When you enable 2FA, don't forget to back up your [recovery codes](#recovery-codes)!
In addition to time-based one time passwords (TOTP), GitLab supports U2F
(universal 2nd factor) and WebAuthn (experimental) devices as the second factor
In addition to time-based one time passwords (TOTP), GitLab supports WebAuthn devices as the second factor
of authentication. After being enabled, in addition to supplying your username
and password to sign in, you're prompted to activate your U2F / WebAuthn device
(usually by pressing a button on it) which performs secure authentication on
@ -269,11 +268,11 @@ Click on **Register U2F Device** to complete the process.
### WebAuthn device
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22506) in GitLab 13.4.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-webauthn).
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22506) in GitLab 13.4 [with a flag](../../../administration/feature_flags.md) named `webauthn`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/232671) in GitLab 14.6.
FLAG:
On self-managed GitLab, by default this feature is available. To disable the feature, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `webauthn`. If you disable the WebAuthn feature flag after WebAuthn devices have been registered, these devices are not usable until you re-enable this feature. On GitLab.com, this feature is available.
The WebAuthn workflow is [supported by](https://caniuse.com/#search=webauthn) the
following desktop browsers:
@ -350,7 +349,7 @@ request, and you're automatically signed in.
### Sign in by using a WebAuthn device
In supported browsers you should be automatically prompted to activate your WebAuthn device
(for example, by touching/pressing its button) after entering your credentials.
(for example, by touching or pressing its button) after entering your credentials.
A message displays, indicating that your device responded to the authentication
request and you're automatically signed in.
@ -495,25 +494,6 @@ request a GitLab global administrator disable two-factor authentication for your
- To enforce 2FA at the system or group levels see [Enforce Two-factor Authentication](../../../security/two_factor_authentication.md).
## Enable or disable WebAuthn **(FREE SELF)**
Support for WebAuthn is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:webauthn)
```
To disable it:
```ruby
Feature.disable(:webauthn)
```
## Troubleshooting
If you are receiving an `invalid pin code` error, this may indicate that there is a time sync issue between the authentication application and the GitLab instance itself.

View File

@ -74,7 +74,7 @@ change and whether you need access to a development environment:
If you decide to permanently stop work on a merge request,
GitLab recommends you close the merge request rather than
[delete it](#delete-a-merge-request). Users with
[delete it](#delete-a-merge-request). The author and assignees of a merge request, and users with
Developer, Maintainer, or Owner [roles](../../permissions.md) in a project
can close merge requests in the project:

View File

@ -32,6 +32,9 @@ You can use project access tokens:
- Consider [disabling project access tokens](#enable-or-disable-project-access-token-creation) to
lower potential abuse.
Project access tokens inherit the [default prefix setting](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix)
configured for personal access tokens.
## Create a project access token
To create a project access token:

View File

@ -22,6 +22,20 @@ ci_namespace_mirrors:
- table: namespaces
column: namespace_id
on_delete: async_delete
ci_builds:
- table: users
column: user_id
on_delete: async_nullify
ci_pipelines:
- table: merge_requests
column: merge_request_id
on_delete: async_delete
- table: external_pull_requests
column: external_pull_request_id
on_delete: async_nullify
- table: users
column: user_id
on_delete: async_nullify
ci_project_mirrors:
- table: projects
column: project_id
@ -49,3 +63,7 @@ merge_request_metrics:
- table: ci_pipelines
column: pipeline_id
on_delete: async_delete
project_pages_metadata:
- table: ci_job_artifacts
column: artifacts_archive_id
on_delete: async_nullify

View File

@ -2,12 +2,6 @@
module Gitlab
module ProcessManagement
# The signals that should terminate both the master and workers.
TERMINATE_SIGNALS = %i(INT TERM).freeze
# The signals that should simply be forwarded to the workers.
FORWARD_SIGNALS = %i(TTIN USR1 USR2 HUP).freeze
# Traps the given signals and yields the block whenever these signals are
# received.
#
@ -26,12 +20,13 @@ module Gitlab
end
end
def self.trap_terminate(&block)
trap_signals(TERMINATE_SIGNALS, &block)
end
def self.trap_forward(&block)
trap_signals(FORWARD_SIGNALS, &block)
# Traps the given signals with the given command.
#
# Example:
#
# modify_signals(%i(HUP TERM), 'DEFAULT')
def self.modify_signals(signals, command)
signals.each { |signal| trap(signal, command) }
end
def self.signal(pid, signal)

View File

@ -1805,6 +1805,15 @@ msgstr ""
msgid "AccessTokens|Are you sure? Any issue email addresses currently in use will stop working."
msgstr ""
msgid "AccessTokens|Copy feed token"
msgstr ""
msgid "AccessTokens|Copy incoming email token"
msgstr ""
msgid "AccessTokens|Copy static object token"
msgstr ""
msgid "AccessTokens|Created"
msgstr ""
@ -1817,12 +1826,21 @@ msgstr ""
msgid "AccessTokens|It cannot be used to access any other data."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
msgstr ""
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}."
msgstr ""
@ -10264,6 +10282,9 @@ msgstr ""
msgid "Crm|Contact has been added"
msgstr ""
msgid "Crm|Contact has been updated"
msgstr ""
msgid "Crm|Create new contact"
msgstr ""
@ -10273,6 +10294,9 @@ msgstr ""
msgid "Crm|Description (optional)"
msgstr ""
msgid "Crm|Edit contact"
msgstr ""
msgid "Crm|Email"
msgstr ""
@ -10282,9 +10306,6 @@ msgstr ""
msgid "Crm|Last name"
msgstr ""
msgid "Crm|New Contact"
msgstr ""
msgid "Crm|New contact"
msgstr ""

View File

@ -22,5 +22,6 @@ require_relative '../lib/gitlab/metrics/exporter/base_exporter'
require_relative '../lib/gitlab/metrics/exporter/sidekiq_exporter'
require_relative '../lib/gitlab/health_checks/probes/collection'
require_relative '../lib/gitlab/health_checks/probes/status'
require_relative '../lib/gitlab/process_management'
# rubocop:enable Naming/FileName

View File

@ -6,17 +6,28 @@ require_relative 'dependencies'
class MetricsServer # rubocop:disable Gitlab/NamespacedClass
class << self
def spawn(target, gitlab_config: nil, wipe_metrics_dir: false)
cmd = "#{Rails.root}/bin/metrics-server"
env = {
'METRICS_SERVER_TARGET' => target,
'GITLAB_CONFIG' => gitlab_config,
'WIPE_METRICS_DIR' => wipe_metrics_dir.to_s
}
def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: [])
raise "The only valid target is 'sidekiq' currently" unless target == 'sidekiq'
Process.spawn(env, cmd, err: $stderr, out: $stdout).tap do |pid|
pid = Process.fork
if pid.nil? # nil means we're inside the fork
# Remove any custom signal handlers the parent process had registered, since we do
# not want to inherit them, and Ruby forks with a `clone` that has the `CLONE_SIGHAND`
# flag set.
Gitlab::ProcessManagement.modify_signals(trapped_signals, 'DEFAULT')
server = MetricsServer.new(target, metrics_dir, wipe_metrics_dir)
# This rewrites /proc/cmdline, since otherwise tools like `top` will show the
# parent process `cmdline` which is really confusing.
$0 = server.name
server.start
else
Process.detach(pid)
end
pid
end
end
@ -34,10 +45,15 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
FileUtils.mkdir_p(@metrics_dir, mode: 0700)
::Prometheus::CleanupMultiprocDirService.new.execute if @wipe_metrics_dir
settings = Settings.monitoring.sidekiq_exporter
settings = Settings.new(Settings.monitoring[name])
exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize
server = exporter_class.instance(settings, synchronous: true)
server.start
end
def name
"#{@target}_exporter"
end
end

View File

@ -55,9 +55,9 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.226.0",
"@gitlab/svgs": "1.229.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "32.43.2",
"@gitlab/ui": "32.49.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.4-1",
"@rails/ujs": "6.1.4-1",
@ -115,7 +115,7 @@
"codesandbox-api": "0.0.23",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
"core-js": "^3.19.3",
"core-js": "^3.20.0",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropper": "^2.3.0",
@ -129,7 +129,7 @@
"dompurify": "^2.3.4",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^7.0.3",
"emoji-regex": "^10.0.0",
"fast-mersenne-twister": "1.0.2",
"file-loader": "^6.2.0",
"fuzzaldrin-plus": "^0.6.0",

View File

@ -20,6 +20,14 @@ require_relative 'sidekiq_cluster'
module Gitlab
module SidekiqCluster
class CLI
THREAD_NAME = 'supervisor'
# The signals that should terminate both the master and workers.
TERMINATE_SIGNALS = %i(INT TERM).freeze
# The signals that should simply be forwarded to the workers.
FORWARD_SIGNALS = %i(TTIN USR1 USR2 HUP).freeze
CommandError = Class.new(StandardError)
def initialize(log_output = $stderr)
@ -27,6 +35,7 @@ module Gitlab
@max_concurrency = 50
@min_concurrency = 0
@environment = ENV['RAILS_ENV'] || 'development'
@metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/sidekiq")
@pid = nil
@interval = 5
@alive = true
@ -39,6 +48,8 @@ module Gitlab
end
def run(argv = ARGV)
Thread.current.name = THREAD_NAME
if argv.empty?
raise CommandError,
'You must specify at least one queue to start a worker for'
@ -144,13 +155,13 @@ module Gitlab
end
def trap_signals
ProcessManagement.trap_terminate do |signal|
ProcessManagement.trap_signals(TERMINATE_SIGNALS) do |signal|
@alive = false
ProcessManagement.signal_processes(@processes, signal)
wait_for_termination
end
ProcessManagement.trap_forward do |signal|
ProcessManagement.trap_signals(FORWARD_SIGNALS) do |signal|
ProcessManagement.signal_processes(@processes, signal)
end
end
@ -180,7 +191,12 @@ module Gitlab
return unless metrics_server_enabled?
@logger.info("Starting metrics server on port #{sidekiq_exporter_port}")
@metrics_server_pid = MetricsServer.spawn('sidekiq', wipe_metrics_dir: wipe_metrics_dir)
@metrics_server_pid = MetricsServer.spawn(
'sidekiq',
metrics_dir: @metrics_dir,
wipe_metrics_dir: wipe_metrics_dir,
trapped_signals: TERMINATE_SIGNALS + FORWARD_SIGNALS
)
end
def sidekiq_exporter_enabled?

View File

@ -29,17 +29,27 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
config_file.write(YAML.dump(config))
config_file.close
@pid = MetricsServer.spawn('sidekiq', gitlab_config: config_file.path, wipe_metrics_dir: true)
env = {
'GITLAB_CONFIG' => config_file.path,
'METRICS_SERVER_TARGET' => 'sidekiq',
'WIPE_METRICS_DIR' => '1'
}
@pid = Process.spawn(env, 'bin/metrics-server', pgroup: true)
end
after do
webmock_enable!
if @pid
pgrp = Process.getpgid(@pid)
Timeout.timeout(5) do
Process.kill('TERM', @pid)
Process.kill('TERM', -pgrp)
Process.waitpid(@pid)
end
expect(Gitlab::ProcessManagement.process_alive?(@pid)).to be(false)
end
rescue Errno::ESRCH => _
# 'No such process' means the process died before

View File

@ -258,6 +258,17 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
end
context 'metrics server' do
let(:trapped_signals) { described_class::TERMINATE_SIGNALS + described_class::FORWARD_SIGNALS }
let(:metrics_dir) { Dir.mktmpdir }
before do
stub_env('prometheus_multiproc_dir', metrics_dir)
end
after do
FileUtils.rm_rf(metrics_dir, secure: true)
end
context 'starting the server' do
context 'without --dryrun' do
context 'when there are no sidekiq_health_checks settings set' do
@ -342,31 +353,33 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
end
end
using RSpec::Parameterized::TableSyntax
context 'with valid settings' do
using RSpec::Parameterized::TableSyntax
where(:sidekiq_exporter_enabled, :sidekiq_exporter_port, :sidekiq_health_checks_port, :start_metrics_server) do
true | '3807' | '3907' | true
true | '3807' | '3807' | false
false | '3807' | '3907' | false
false | '3807' | '3907' | false
end
with_them do
before do
allow(Gitlab::SidekiqCluster).to receive(:start)
allow(cli).to receive(:write_pid)
allow(cli).to receive(:trap_signals)
allow(cli).to receive(:start_loop)
where(:sidekiq_exporter_enabled, :sidekiq_exporter_port, :sidekiq_health_checks_port, :start_metrics_server) do
true | '3807' | '3907' | true
true | '3807' | '3807' | false
false | '3807' | '3907' | false
false | '3807' | '3907' | false
end
specify do
if start_metrics_server
expect(MetricsServer).to receive(:spawn).with('sidekiq', wipe_metrics_dir: true)
else
expect(MetricsServer).not_to receive(:spawn)
with_them do
before do
allow(Gitlab::SidekiqCluster).to receive(:start)
allow(cli).to receive(:write_pid)
allow(cli).to receive(:trap_signals)
allow(cli).to receive(:start_loop)
end
cli.run(%w(foo))
specify do
if start_metrics_server
expect(MetricsServer).to receive(:spawn).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, trapped_signals: trapped_signals)
else
expect(MetricsServer).not_to receive(:spawn)
end
cli.run(%w(foo))
end
end
end
end
@ -388,7 +401,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
before do
allow(cli).to receive(:sleep).with(a_kind_of(Numeric))
allow(MetricsServer).to receive(:spawn).with('sidekiq', wipe_metrics_dir: false).and_return(99)
allow(MetricsServer).to receive(:spawn).and_return(99)
cli.start_metrics_server
end
@ -407,7 +420,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
allow(Gitlab::ProcessManagement).to receive(:all_alive?).with(an_instance_of(Array)).and_return(false)
allow(cli).to receive(:stop_metrics_server)
expect(MetricsServer).to receive(:spawn).with('sidekiq', wipe_metrics_dir: false)
expect(MetricsServer).to receive(:spawn).with(
'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, trapped_signals: trapped_signals
)
cli.start_loop
end
@ -484,9 +499,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
end
describe '#trap_signals' do
it 'traps the termination and forwarding signals' do
expect(Gitlab::ProcessManagement).to receive(:trap_terminate)
expect(Gitlab::ProcessManagement).to receive(:trap_forward)
it 'traps termination and sidekiq specific signals' do
expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[INT TERM])
expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[TTIN USR1 USR2 HUP])
cli.trap_signals
end

View File

@ -26,9 +26,8 @@ RSpec.describe 'Database schema' do
boards: %w[milestone_id iteration_id],
chat_names: %w[chat_id team_id user_id],
chat_teams: %w[team_id],
ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
ci_builds: %w[erased_by_id runner_id trigger_request_id],
ci_namespace_monthly_usages: %w[namespace_id],
ci_pipelines: %w[user_id],
ci_runner_projects: %w[runner_id],
ci_trigger_requests: %w[commit_id],
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],

View File

@ -323,6 +323,14 @@ FactoryBot.define do
size { 1149.bytes }
end
trait(:generic_zip) do
package
file_fixture { 'spec/fixtures/packages/generic/myfile.zip' }
file_name { "#{package.name}.zip" }
file_sha256 { '3559e770bd493b326e8ec5e6242f7206d3fbf94fa47c16f82d34a037daa113e5' }
size { 3989.bytes }
end
trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE }
end

View File

@ -247,6 +247,12 @@ FactoryBot.define do
sequence(:name) { |n| "generic-package-#{n}" }
version { '1.0.0' }
package_type { :generic }
trait(:with_zip_file) do
after :create do |package|
create :package_file, :generic_zip, package: package
end
end
end
end
end

View File

@ -57,6 +57,14 @@ RSpec.describe 'Global search' do
expect(page).to have_selector('.search-form')
expect(page).to have_no_selector('#js-header-search')
end
it 'focuses search input when shortcut "s" is pressed', :js do
expect(page).not_to have_selector('#search:focus')
find('body').native.send_key('s')
expect(page).to have_selector('#search:focus')
end
end
describe 'when new_header_search feature is enabled' do
@ -70,5 +78,13 @@ RSpec.describe 'Global search' do
expect(page).to have_no_selector('.search-form')
expect(page).to have_selector('#js-header-search')
end
it 'focuses search input when shortcut "s" is pressed', :js do
expect(page).not_to have_selector('#search:focus')
find('body').native.send_key('s')
expect(page).to have_selector('#search:focus')
end
end
end

View File

@ -63,11 +63,24 @@ RSpec.describe 'Profile account page', :js do
end
describe 'when I reset feed token' do
before do
it 'resets feed token with `hide_access_tokens` feature flag enabled' do
visit profile_personal_access_tokens_path
within('[data-testid="feed-token-container"]') do
previous_token = find_field('Feed token').value
accept_confirm { click_link('reset this token') }
click_button('Click to reveal')
expect(find_field('Feed token').value).not_to eq(previous_token)
end
end
it 'resets feed token' do
it 'resets feed token with `hide_access_tokens` feature flag disabled' do
stub_feature_flags(hide_access_tokens: false)
visit profile_personal_access_tokens_path
within('.feed-token-reset') do
previous_token = find("#feed_token").value
@ -82,10 +95,26 @@ RSpec.describe 'Profile account page', :js do
before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
stub_feature_flags(bootstrap_confirmation_modals: false)
visit profile_personal_access_tokens_path
end
it 'resets incoming email token' do
it 'resets incoming email token with `hide_access_tokens` feature flag enabled' do
visit profile_personal_access_tokens_path
within('[data-testid="incoming-email-token-container"]') do
previous_token = find_field('Incoming email token').value
accept_confirm { click_link('reset this token') }
click_button('Click to reveal')
expect(find_field('Incoming email token').value).not_to eq(previous_token)
end
end
it 'resets incoming email token with `hide_access_tokens` feature flag disabled' do
stub_feature_flags(hide_access_tokens: false)
visit profile_personal_access_tokens_path
within('.incoming-email-token-reset') do
previous_token = find('#incoming_email_token').value

View File

@ -18,10 +18,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
find("#created-personal-access-token").value
end
def feed_token
find("#feed_token").value
end
def feed_token_description
"Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs."
end
@ -136,12 +132,24 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
describe "feed token" do
context "when enabled" do
it "displays feed token" do
it "displays feed token with `hide_access_tokens` feature flag enabled" do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
visit profile_personal_access_tokens_path
expect(feed_token).to eq(user.feed_token)
within('[data-testid="feed-token-container"]') do
click_button('Click to reveal')
expect(page).to have_field('Feed token', with: user.feed_token)
expect(page).to have_content(feed_token_description)
end
end
it "displays feed token with `hide_access_tokens` feature flag disabled" do
stub_feature_flags(hide_access_tokens: false)
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
visit profile_personal_access_tokens_path
expect(page).to have_field('Feed token', with: user.feed_token)
expect(page).to have_content(feed_token_description)
end
end
@ -151,8 +159,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
visit profile_personal_access_tokens_path
expect(page).to have_no_content(feed_token_description)
expect(page).to have_no_css("#feed_token")
expect(page).not_to have_content(feed_token_description)
expect(page).not_to have_field('Feed token')
end
end
end

Binary file not shown.

View File

@ -1,8 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
import axios from '~/lib/utils/axios_utils';
import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants';
export const emojiFixtureMap = {
export const validEmoji = {
atom: {
moji: '⚛',
description: 'atom symbol',
@ -49,11 +48,39 @@ export const emojiFixtureMap = {
unicodeVersion: '5.1',
description: 'white medium star',
},
gay_pride_flag: {
moji: '🏳️‍🌈',
unicodeVersion: '7.0',
description: 'because it contains a zero width joiner',
},
family_mmb: {
moji: '👨‍👨‍👦',
unicodeVersion: '6.0',
description: 'because it contains multiple zero width joiners',
},
};
export const invalidEmoji = {
xss: {
moji: '<img src=x onerror=prompt(1)>',
unicodeVersion: '5.1',
description: 'xss',
},
non_moji: {
moji: 'I am not an emoji...',
unicodeVersion: '9.0',
description: '...and should be filtered out',
},
multiple_moji: {
moji: '🍂🏭',
unicodeVersion: '9.0',
description: 'Multiple separate emoji that are not joined by a zero width joiner',
},
};
export const emojiFixtureMap = {
...validEmoji,
...invalidEmoji,
};
export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
@ -63,11 +90,14 @@ export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
return acc;
}, {});
export async function initEmojiMock(mockData = mockEmojiData) {
const mock = new MockAdapter(axios);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(mockData));
await initEmojiMap();
return mock;
export function clearEmojiMock() {
localStorage.clear();
initEmojiMap.promise = null;
}
export async function initEmojiMock(mockData = mockEmojiData) {
clearEmojiMock();
localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
localStorage.setItem(CACHE_KEY, JSON.stringify(mockData));
await initEmojiMap();
}

View File

@ -0,0 +1,65 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import Token from '~/access_tokens/components/token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
describe('Token', () => {
let wrapper;
const defaultPropsData = {
token: 'az4a2l5f8ssa0zvdfbhidbzlx',
inputId: 'feed_token',
inputLabel: 'Feed token',
copyButtonTitle: 'Copy feed token',
};
const defaultSlots = {
title: 'Feed token title',
description: 'Feed token description',
'input-description': 'Feed token input description',
};
const createComponent = () => {
wrapper = mountExtended(Token, { propsData: defaultPropsData, slots: defaultSlots });
};
afterEach(() => {
wrapper.destroy();
});
it('renders title slot', () => {
createComponent();
expect(wrapper.findByText(defaultSlots.title, { selector: 'h4' }).exists()).toBe(true);
});
it('renders description slot', () => {
createComponent();
expect(wrapper.findByText(defaultSlots.description).exists()).toBe(true);
});
it('renders input description slot', () => {
createComponent();
expect(wrapper.findByText(defaultSlots['input-description']).exists()).toBe(true);
});
it('correctly passes props to `InputCopyToggleVisibility` component', () => {
createComponent();
const inputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility);
expect(inputCopyToggleVisibilityComponent.props()).toMatchObject({
formInputGroupProps: {
id: defaultPropsData.inputId,
},
value: defaultPropsData.token,
copyButtonTitle: defaultPropsData.copyButtonTitle,
});
expect(inputCopyToggleVisibilityComponent.attributes()).toMatchObject({
label: defaultPropsData.inputLabel,
'label-for': defaultPropsData.inputId,
});
});
});

View File

@ -0,0 +1,148 @@
import { merge } from 'lodash';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import TokensApp from '~/access_tokens/components/tokens_app.vue';
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants';
describe('TokensApp', () => {
let wrapper;
const defaultProvide = {
tokenTypes: {
[FEED_TOKEN]: {
enabled: true,
token: 'DUKu345VD73Py7zz3z89',
resetPath: '/-/profile/reset_feed_token',
},
[INCOMING_EMAIL_TOKEN]: {
enabled: true,
token: 'az4a2l5f8ssa0zvdfbhidbzlx',
resetPath: '/-/profile/reset_incoming_email_token',
},
[STATIC_OBJECT_TOKEN]: {
enabled: true,
token: 'QHXwGHYioHTgxQnAcyZ-',
resetPath: '/-/profile/reset_static_object_token',
},
},
};
const createComponent = (options = {}) => {
wrapper = mountExtended(TokensApp, merge({}, { provide: defaultProvide }, options));
};
const expectTokenRendered = ({
testId,
expectedLabel,
expectedDescription,
expectedInputDescription,
expectedResetPath,
expectedResetConfirmMessage,
expectedProps,
}) => {
const container = extendedWrapper(wrapper.findByTestId(testId));
expect(container.findByText(expectedLabel, { selector: 'h4' }).exists()).toBe(true);
expect(container.findByText(expectedDescription).exists()).toBe(true);
expect(container.findByText(expectedInputDescription, { exact: false }).exists()).toBe(true);
expect(container.findByText('reset this token').attributes()).toMatchObject({
'data-confirm': expectedResetConfirmMessage,
'data-method': 'put',
href: expectedResetPath,
});
expect(container.props()).toMatchObject(expectedProps);
};
afterEach(() => {
wrapper.destroy();
});
it('renders all enabled tokens', () => {
createComponent();
expectTokenRendered({
testId: TokensApp.htmlAttributes[FEED_TOKEN].containerTestId,
expectedLabel: TokensApp.i18n[FEED_TOKEN].label,
expectedDescription: TokensApp.i18n[FEED_TOKEN].description,
expectedInputDescription:
'Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you.',
expectedResetPath: defaultProvide.tokenTypes[FEED_TOKEN].resetPath,
expectedResetConfirmMessage: TokensApp.i18n[FEED_TOKEN].resetConfirmMessage,
expectedProps: {
token: defaultProvide.tokenTypes[FEED_TOKEN].token,
inputId: TokensApp.htmlAttributes[FEED_TOKEN].inputId,
inputLabel: TokensApp.i18n[FEED_TOKEN].label,
copyButtonTitle: TokensApp.i18n[FEED_TOKEN].copyButtonTitle,
},
});
expectTokenRendered({
testId: TokensApp.htmlAttributes[INCOMING_EMAIL_TOKEN].containerTestId,
expectedLabel: TokensApp.i18n[INCOMING_EMAIL_TOKEN].label,
expectedDescription: TokensApp.i18n[INCOMING_EMAIL_TOKEN].description,
expectedInputDescription:
'Keep this token secret. Anyone who has it can create issues as if they were you.',
expectedResetPath: defaultProvide.tokenTypes[INCOMING_EMAIL_TOKEN].resetPath,
expectedResetConfirmMessage: TokensApp.i18n[INCOMING_EMAIL_TOKEN].resetConfirmMessage,
expectedProps: {
token: defaultProvide.tokenTypes[INCOMING_EMAIL_TOKEN].token,
inputId: TokensApp.htmlAttributes[INCOMING_EMAIL_TOKEN].inputId,
inputLabel: TokensApp.i18n[INCOMING_EMAIL_TOKEN].label,
copyButtonTitle: TokensApp.i18n[INCOMING_EMAIL_TOKEN].copyButtonTitle,
},
});
expectTokenRendered({
testId: TokensApp.htmlAttributes[STATIC_OBJECT_TOKEN].containerTestId,
expectedLabel: TokensApp.i18n[STATIC_OBJECT_TOKEN].label,
expectedDescription: TokensApp.i18n[STATIC_OBJECT_TOKEN].description,
expectedInputDescription:
'Keep this token secret. Anyone who has it can access repository static objects as if they were you.',
expectedResetPath: defaultProvide.tokenTypes[STATIC_OBJECT_TOKEN].resetPath,
expectedResetConfirmMessage: TokensApp.i18n[STATIC_OBJECT_TOKEN].resetConfirmMessage,
expectedProps: {
token: defaultProvide.tokenTypes[STATIC_OBJECT_TOKEN].token,
inputId: TokensApp.htmlAttributes[STATIC_OBJECT_TOKEN].inputId,
inputLabel: TokensApp.i18n[STATIC_OBJECT_TOKEN].label,
copyButtonTitle: TokensApp.i18n[STATIC_OBJECT_TOKEN].copyButtonTitle,
},
});
});
it("doesn't render disabled tokens", () => {
createComponent({
provide: {
tokenTypes: {
[FEED_TOKEN]: {
enabled: false,
},
},
},
});
expect(
wrapper.findByTestId(TokensApp.htmlAttributes[FEED_TOKEN].containerTestId).exists(),
).toBe(false);
});
describe('when there are tokens missing an `i18n` definition', () => {
it('renders without errors', () => {
createComponent({
provide: {
tokenTypes: {
fooBar: {
enabled: true,
token: 'rewjoa58dfm54jfkdlsdf',
resetPath: '/-/profile/foo_bar',
},
},
},
});
expect(
wrapper.findByTestId(TokensApp.htmlAttributes[FEED_TOKEN].containerTestId).exists(),
).toBe(true);
});
});
});

View File

@ -12,6 +12,7 @@ exports[`Alert integration settings form default state should match the default
<gl-form-group-stub
class="gl-pl-0"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-checkbox-stub
checked="true"
@ -28,6 +29,7 @@ exports[`Alert integration settings form default state should match the default
label-for="alert-integration-settings-issue-template"
label-size="sm"
labeldescription=""
optionaltext="(optional)"
>
<label
class="gl-display-inline-flex"
@ -83,6 +85,7 @@ exports[`Alert integration settings form default state should match the default
<gl-form-group-stub
class="gl-pl-0 gl-mb-5"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-checkbox-stub>
<span>
@ -94,6 +97,7 @@ exports[`Alert integration settings form default state should match the default
<gl-form-group-stub
class="gl-pl-0 gl-mb-5"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-checkbox-stub
checked="true"

View File

@ -1,15 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import Cookies from 'js-cookie';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import loadAwardsHandler from '~/awards_handler';
import { EMOJI_VERSION } from '~/emoji';
import axios from '~/lib/utils/axios_utils';
window.gl = window.gl || {};
window.gon = window.gon || {};
let mock;
let awardsHandler = null;
const urlRoot = gon.relative_url_root;
@ -76,8 +73,7 @@ describe('AwardsHandler', () => {
};
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
await initEmojiMock(emojiData);
loadFixtures('snippets/show.html');
@ -89,7 +85,7 @@ describe('AwardsHandler', () => {
// restore original url root value
gon.relative_url_root = urlRoot;
mock.restore();
clearEmojiMock();
// Undo what we did to the shared <body>
$('body').removeAttr('data-page');

View File

@ -1,15 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import waitForPromises from 'helpers/wait_for_promises';
import installGlEmojiElement from '~/behaviors/gl_emoji';
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
import { EMOJI_VERSION } from '~/emoji';
import * as EmojiUnicodeSupport from '~/emoji/support';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/emoji/support');
describe('gl_emoji', () => {
let mock;
const emojiData = {
grey_question: {
c: 'symbols',
@ -38,15 +36,12 @@ describe('gl_emoji', () => {
return div.firstElementChild;
}
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
return initEmojiMap().catch(() => {});
beforeEach(async () => {
await initEmojiMock(emojiData);
});
afterEach(() => {
mock.restore();
clearEmojiMock();
document.body.innerHTML = '';
});

View File

@ -4,41 +4,49 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewContactForm from '~/crm/components/new_contact_form.vue';
import ContactForm from '~/crm/components/contact_form.vue';
import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
import {
createContactMutationErrorResponse,
createContactMutationResponse,
getGroupContactsQueryResponse,
updateContactMutationErrorResponse,
updateContactMutationResponse,
} from './mock_data';
describe('Customer relations contacts root app', () => {
describe('Customer relations contact form component', () => {
Vue.use(VueApollo);
let wrapper;
let fakeApollo;
let mutation;
let queryHandler;
const findCreateNewContactButton = () => wrapper.findByTestId('create-new-contact-button');
const findSaveContactButton = () => wrapper.findByTestId('save-contact-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findForm = () => wrapper.find('form');
const findError = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ mountFunction = shallowMountExtended } = {}) => {
fakeApollo = createMockApollo([[createContactMutation, queryHandler]]);
const mountComponent = ({ mountFunction = shallowMountExtended, editForm = false } = {}) => {
fakeApollo = createMockApollo([[mutation, queryHandler]]);
fakeApollo.clients.defaultClient.cache.writeQuery({
query: getGroupContactsQuery,
variables: { groupFullPath: 'flightjs' },
data: getGroupContactsQueryResponse.data,
});
wrapper = mountFunction(NewContactForm, {
const propsData = { drawerOpen: true };
if (editForm)
propsData.contact = { firstName: 'First', lastName: 'Last', email: 'email@example.com' };
wrapper = mountFunction(ContactForm, {
provide: { groupId: 26, groupFullPath: 'flightjs' },
apolloProvider: fakeApollo,
propsData: { drawerOpen: true },
propsData,
});
};
beforeEach(() => {
mutation = createContactMutation;
queryHandler = jest.fn().mockResolvedValue(createContactMutationResponse);
});
@ -47,14 +55,14 @@ describe('Customer relations contacts root app', () => {
fakeApollo = null;
});
describe('Create new contact button', () => {
it('should be disabled by default', () => {
describe('Save contact button', () => {
it('should be disabled when required fields are empty', () => {
mountComponent();
expect(findCreateNewContactButton().attributes('disabled')).toBeTruthy();
expect(findSaveContactButton().props('disabled')).toBe(true);
});
it('should not be disabled when first, last and email have values', async () => {
it('should not be disabled when required fields have values', async () => {
mountComponent();
wrapper.find('#contact-first-name').vm.$emit('input', 'A');
@ -62,7 +70,7 @@ describe('Customer relations contacts root app', () => {
wrapper.find('#contact-email').vm.$emit('input', 'C');
await waitForPromises();
expect(findCreateNewContactButton().attributes('disabled')).toBeFalsy();
expect(findSaveContactButton().props('disabled')).toBe(false);
});
});
@ -74,7 +82,7 @@ describe('Customer relations contacts root app', () => {
expect(wrapper.emitted().close).toBeTruthy();
});
describe('when query is successful', () => {
describe('when create mutation is successful', () => {
it("should emit 'close'", async () => {
mountComponent();
@ -85,7 +93,7 @@ describe('Customer relations contacts root app', () => {
});
});
describe('when query fails', () => {
describe('when create mutation fails', () => {
it('should show error on reject', async () => {
queryHandler = jest.fn().mockRejectedValue('ERROR');
mountComponent();
@ -107,4 +115,43 @@ describe('Customer relations contacts root app', () => {
expect(findError().text()).toBe('Phone is invalid.');
});
});
describe('when update mutation is successful', () => {
it("should emit 'close'", async () => {
mutation = updateContactMutation;
queryHandler = jest.fn().mockResolvedValue(updateContactMutationResponse);
mountComponent({ editForm: true });
findForm().trigger('submit');
await waitForPromises();
expect(wrapper.emitted().close).toBeTruthy();
});
});
describe('when update mutation fails', () => {
beforeEach(() => {
mutation = updateContactMutation;
});
it('should show error on reject', async () => {
queryHandler = jest.fn().mockRejectedValue('ERROR');
mountComponent({ editForm: true });
findForm().trigger('submit');
await waitForPromises();
expect(findError().exists()).toBe(true);
});
it('should show error on error response', async () => {
queryHandler = jest.fn().mockResolvedValue(updateContactMutationErrorResponse);
mountComponent({ editForm: true });
findForm().trigger('submit');
await waitForPromises();
expect(findError().exists()).toBe(true);
expect(findError().text()).toBe('Email is invalid.');
});
});
});

View File

@ -6,8 +6,10 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ContactsRoot from '~/crm/components/contacts_root.vue';
import NewContactForm from '~/crm/components/new_contact_form.vue';
import ContactForm from '~/crm/components/contact_form.vue';
import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
import { NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '~/crm/constants';
import routes from '~/crm/routes';
import { getGroupContactsQueryResponse } from './mock_data';
describe('Customer relations contacts root app', () => {
@ -21,7 +23,8 @@ describe('Customer relations contacts root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewContactButton = () => wrapper.findByTestId('new-contact-button');
const findNewContactForm = () => wrapper.findComponent(NewContactForm);
const findEditContactButton = () => wrapper.findByTestId('edit-contact-button');
const findContactForm = () => wrapper.findComponent(ContactForm);
const findError = () => wrapper.findComponent(GlAlert);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
@ -49,7 +52,7 @@ describe('Customer relations contacts root app', () => {
router = new VueRouter({
base: basePath,
mode: 'history',
routes: [],
routes,
});
});
@ -79,12 +82,12 @@ describe('Customer relations contacts root app', () => {
});
});
describe('new contact form', () => {
describe('contact form', () => {
it('should not exist by default', async () => {
mountComponent();
await waitForPromises();
expect(findNewContactForm().exists()).toBe(false);
expect(findContactForm().exists()).toBe(false);
});
it('should exist when user clicks new contact button', async () => {
@ -93,25 +96,54 @@ describe('Customer relations contacts root app', () => {
findNewContactButton().vm.$emit('click');
await waitForPromises();
expect(findNewContactForm().exists()).toBe(true);
expect(findContactForm().exists()).toBe(true);
});
it('should exist when user navigates directly to /new', async () => {
router.replace({ path: '/new' });
it('should exist when user navigates directly to `new` route', async () => {
router.replace({ name: NEW_ROUTE_NAME });
mountComponent();
await waitForPromises();
expect(findNewContactForm().exists()).toBe(true);
expect(findContactForm().exists()).toBe(true);
});
it('should not exist when form emits close', async () => {
router.replace({ path: '/new' });
mountComponent();
findNewContactForm().vm.$emit('close');
it('should exist when user clicks edit contact button', async () => {
mountComponent({ mountFunction: mountExtended });
await waitForPromises();
expect(findNewContactForm().exists()).toBe(false);
findEditContactButton().vm.$emit('click');
await waitForPromises();
expect(findContactForm().exists()).toBe(true);
});
it('should exist when user navigates directly to `edit` route', async () => {
router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
mountComponent();
await waitForPromises();
expect(findContactForm().exists()).toBe(true);
});
it('should not exist when new form emits close', async () => {
router.replace({ name: NEW_ROUTE_NAME });
mountComponent();
findContactForm().vm.$emit('close');
await waitForPromises();
expect(findContactForm().exists()).toBe(false);
});
it('should not exist when edit form emits close', async () => {
router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
mountComponent();
await waitForPromises();
findContactForm().vm.$emit('close');
await waitForPromises();
expect(findContactForm().exists()).toBe(false);
});
});

View File

@ -106,3 +106,31 @@ export const createContactMutationErrorResponse = {
},
},
};
export const updateContactMutationResponse = {
data: {
customerRelationsContactUpdate: {
__typeName: 'CustomerRelationsContactCreatePayload',
contact: {
__typename: 'CustomerRelationsContact',
id: 'gid://gitlab/CustomerRelations::Contact/1',
firstName: 'First',
lastName: 'Last',
email: 'email@example.com',
phone: null,
description: null,
organization: null,
},
errors: [],
},
},
};
export const updateContactMutationErrorResponse = {
data: {
customerRelationsContactUpdate: {
contact: null,
errors: ['Email is invalid.'],
},
},
};

View File

@ -1,6 +1,21 @@
import { emojiFixtureMap, mockEmojiData, initEmojiMock } from 'helpers/emoji';
import {
emojiFixtureMap,
mockEmojiData,
initEmojiMock,
validEmoji,
invalidEmoji,
clearEmojiMock,
} from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
import { glEmojiTag, searchEmoji, getEmojiInfo, sortEmoji } from '~/emoji';
import {
glEmojiTag,
searchEmoji,
getEmojiInfo,
sortEmoji,
initEmojiMap,
getAllEmoji,
} from '~/emoji';
import isEmojiUnicodeSupported, {
isFlagEmoji,
isRainbowFlagEmoji,
@ -9,7 +24,6 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
import { sanitize } from '~/lib/dompurify';
const emptySupportMap = {
personZwj: false,
@ -31,14 +45,55 @@ const emptySupportMap = {
};
describe('emoji', () => {
let mock;
beforeEach(async () => {
mock = await initEmojiMock();
await initEmojiMock();
});
afterEach(() => {
mock.restore();
clearEmojiMock();
});
describe('initEmojiMap', () => {
it('should contain valid emoji', async () => {
await initEmojiMap();
const allEmoji = Object.keys(getAllEmoji());
Object.keys(validEmoji).forEach((key) => {
expect(allEmoji.includes(key)).toBe(true);
});
});
it('should not contain invalid emoji', async () => {
await initEmojiMap();
const allEmoji = Object.keys(getAllEmoji());
Object.keys(invalidEmoji).forEach((key) => {
expect(allEmoji.includes(key)).toBe(false);
});
});
it('fixes broken pride emoji', async () => {
clearEmojiMock();
await initEmojiMock({
gay_pride_flag: {
c: 'flags',
// Without a zero-width joiner
e: '🏳🌈',
name: 'gay_pride_flag',
u: '6.0',
},
});
expect(getAllEmoji()).toEqual({
gay_pride_flag: {
c: 'flags',
// With a zero-width joiner
e: '🏳️‍🌈',
name: 'gay_pride_flag',
u: '6.0',
},
});
});
});
describe('glEmojiTag', () => {
@ -378,32 +433,14 @@ describe('emoji', () => {
});
describe('searchEmoji', () => {
const emojiFixture = Object.keys(mockEmojiData).reduce((acc, k) => {
const { name, e, u, d } = mockEmojiData[k];
acc[k] = { name, e: sanitize(e), u, d };
return acc;
}, {});
it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => {
const search = searchEmoji(input);
const expected = [
'atom',
'bomb',
'construction_worker_tone5',
'five',
'grey_question',
'black_heart',
'heart',
'custard',
'star',
'xss',
].map((name) => {
const expected = Object.keys(validEmoji).map((name) => {
return {
emoji: emojiFixture[name],
emoji: mockEmojiData[name],
field: 'd',
fieldValue: emojiFixture[name].d,
fieldValue: mockEmojiData[name].d,
score: 0,
};
});
@ -453,7 +490,7 @@ describe('emoji', () => {
const { field, score, fieldValue, name } = item;
return {
emoji: emojiFixture[name],
emoji: mockEmojiData[name],
field,
fieldValue,
score,
@ -564,9 +601,9 @@ describe('emoji', () => {
const { field, score, name } = item;
return {
emoji: emojiFixture[name],
emoji: mockEmojiData[name],
field,
fieldValue: emojiFixture[name][field],
fieldValue: mockEmojiData[name][field],
score,
};
});
@ -622,13 +659,4 @@ describe('emoji', () => {
expect(sortEmoji(scoredItems)).toEqual(expected);
});
});
describe('sanitize emojis', () => {
it('should return sanitized emoji', () => {
expect(getEmojiInfo('xss')).toEqual({
...mockEmojiData.xss,
e: '<img src="x">',
});
});
});
});

View File

@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete';
import { initEmojiMock } from 'helpers/emoji';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
@ -803,8 +803,6 @@ describe('GfmAutoComplete', () => {
});
describe('emoji', () => {
let mock;
const mockItem = {
'atwho-at': ':',
emoji: {
@ -818,14 +816,14 @@ describe('GfmAutoComplete', () => {
};
beforeEach(async () => {
mock = await initEmojiMock();
await initEmojiMock();
await new GfmAutoComplete({}).loadEmojiData({ atwho() {}, trigger() {} }, ':');
if (!GfmAutoComplete.glEmojiTag) throw new Error('emoji not loaded');
});
afterEach(() => {
mock.restore();
clearEmojiMock();
});
describe('Emoji.templateFunction', () => {

View File

@ -47,6 +47,7 @@ exports[`grafana integration component default state to match the default snapsh
label="Enable authentication"
label-for="grafana-integration-enabled"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-checkbox-stub
id="grafana-integration-enabled"
@ -62,6 +63,7 @@ exports[`grafana integration component default state to match the default snapsh
label="Grafana URL"
label-for="grafana-url"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-input-stub
id="grafana-url"
@ -74,6 +76,7 @@ exports[`grafana integration component default state to match the default snapsh
label="API token"
label-for="grafana-token"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-input-stub
id="grafana-token"

View File

@ -14,6 +14,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
<gl-form-group-stub
class="col-8 col-md-9 gl-p-0"
labeldescription=""
optionaltext="(optional)"
>
<gl-toggle-stub
id="active"
@ -28,6 +29,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
label="Webhook URL"
label-for="url"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-input-group-stub
data-testid="webhook-url"

View File

@ -52,6 +52,7 @@ exports[`self monitor component When the self monitor project has not been creat
<gl-form-group-stub
labeldescription=""
optionaltext="(optional)"
>
<gl-toggle-stub
label="Self monitoring"

View File

@ -1,6 +1,6 @@
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { initEmojiMock } from 'helpers/emoji';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
import createFlash from '~/flash';
@ -12,7 +12,6 @@ jest.mock('~/flash');
describe('SetStatusModalWrapper', () => {
let wrapper;
let mockEmoji;
const $toast = {
show: jest.fn(),
};
@ -63,12 +62,12 @@ describe('SetStatusModalWrapper', () => {
afterEach(() => {
wrapper.destroy();
mockEmoji.restore();
clearEmojiMock();
});
describe('with minimum props', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent();
return initModal();
});
@ -112,7 +111,7 @@ describe('SetStatusModalWrapper', () => {
describe('improvedEmojiPicker is true', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent({}, true);
return initModal();
});
@ -126,7 +125,7 @@ describe('SetStatusModalWrapper', () => {
describe('with no currentMessage set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent({ currentMessage: '' });
return initModal();
});
@ -146,7 +145,7 @@ describe('SetStatusModalWrapper', () => {
describe('with no currentEmoji set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent({ currentEmoji: '' });
return initModal();
});
@ -161,7 +160,7 @@ describe('SetStatusModalWrapper', () => {
describe('with no currentMessage set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
return initModal();
});
@ -174,7 +173,7 @@ describe('SetStatusModalWrapper', () => {
describe('with currentClearStatusAfter set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent({ currentClearStatusAfter: '2021-01-01 00:00:00 UTC' });
return initModal();
});
@ -190,7 +189,7 @@ describe('SetStatusModalWrapper', () => {
describe('update status', () => {
describe('succeeds', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent();
await initModal();
@ -246,7 +245,7 @@ describe('SetStatusModalWrapper', () => {
describe('success message', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
return initModal({ mockOnUpdateSuccess: false });
@ -262,7 +261,7 @@ describe('SetStatusModalWrapper', () => {
describe('with errors', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent();
await initModal();
@ -279,7 +278,7 @@ describe('SetStatusModalWrapper', () => {
describe('error message', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
return initModal({ mockOnUpdateFailure: false });

View File

@ -25,6 +25,7 @@ describe('Shortcuts', () => {
jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus');
jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus');
jest.spyOn(document.querySelector('#search'), 'focus');
new Shortcuts(); // eslint-disable-line no-new
});
@ -111,4 +112,12 @@ describe('Shortcuts', () => {
});
});
});
describe('focusSearch', () => {
it('focuses the search bar', () => {
Shortcuts.focusSearch(createEvent('KeyboardEvent'));
expect(document.querySelector('#search').focus).toHaveBeenCalled();
});
});
});

View File

@ -23,6 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
class="gl-mb-0"
id="visibility-level-setting"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-radio-group-stub
checked="private"

View File

@ -5,6 +5,7 @@ exports[`Title edit field matches the snapshot 1`] = `
label="Title"
label-for="title-field-edit"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-input-stub />
</gl-form-group-stub>

View File

@ -15,4 +15,53 @@ RSpec.describe AccessTokensHelper do
it { expect(helper.scope_description(prefix)).to eq(description_location) }
end
end
describe '#tokens_app_data' do
let_it_be(:feed_token) { 'DUKu345VD73Py7zz3z89' }
let_it_be(:incoming_email_token) { 'az4a2l5f8ssa0zvdfbhidbzlx' }
let_it_be(:static_object_token) { 'QHXwGHYioHTgxQnAcyZ-' }
let_it_be(:feed_token_reset_path) { '/-/profile/reset_feed_token' }
let_it_be(:incoming_email_token_reset_path) { '/-/profile/reset_incoming_email_token' }
let_it_be(:static_object_token_reset_path) { '/-/profile/reset_static_object_token' }
let_it_be(:user) do
build(
:user,
feed_token: feed_token,
incoming_email_token: incoming_email_token,
static_object_token: static_object_token
)
end
it 'returns expected json' do
allow(Gitlab::CurrentSettings).to receive_messages(
disable_feed_token: false,
static_objects_external_storage_enabled?: true
)
allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(true)
allow(helper).to receive_messages(
current_user: user,
reset_feed_token_profile_path: feed_token_reset_path,
reset_incoming_email_token_profile_path: incoming_email_token_reset_path,
reset_static_object_token_profile_path: static_object_token_reset_path
)
expect(helper.tokens_app_data).to eq({
feed_token: {
enabled: true,
token: feed_token,
reset_path: feed_token_reset_path
},
incoming_email_token: {
enabled: true,
token: incoming_email_token,
reset_path: incoming_email_token_reset_path
},
static_object_token: {
enabled: true,
token: static_object_token,
reset_path: static_object_token_reset_path
}
}.to_json)
end
end
end

View File

@ -12,21 +12,12 @@ RSpec.describe Gitlab::ProcessManagement do
end
end
describe '.trap_terminate' do
it 'traps the termination signals' do
expect(described_class).to receive(:trap_signals)
.with(described_class::TERMINATE_SIGNALS)
describe '.modify_signals' do
it 'traps the given signals with the given command' do
expect(described_class).to receive(:trap).ordered.with(:INT, 'DEFAULT')
expect(described_class).to receive(:trap).ordered.with(:HUP, 'DEFAULT')
described_class.trap_terminate { }
end
end
describe '.trap_forward' do
it 'traps the signals to forward' do
expect(described_class).to receive(:trap_signals)
.with(described_class::FORWARD_SIGNALS)
described_class.trap_forward { }
described_class.modify_signals(%i(INT HUP), 'DEFAULT')
end
end

View File

@ -8,52 +8,67 @@ require_relative '../support/helpers/next_instance_of'
RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
include NextInstanceOf
before do
# We do not want this to have knock-on effects on the test process.
allow(Gitlab::ProcessManagement).to receive(:modify_signals)
end
describe '.spawn' do
let(:env) do
{
'METRICS_SERVER_TARGET' => 'sidekiq',
'GITLAB_CONFIG' => nil,
'WIPE_METRICS_DIR' => 'false'
}
context 'when in parent process' do
it 'forks into a new process and detaches it' do
expect(Process).to receive(:fork).and_return(99)
expect(Process).to receive(:detach).with(99)
described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics')
end
end
it 'spawns a process with the correct environment variables and detaches it' do
expect(Process).to receive(:spawn).with(env, anything, err: $stderr, out: $stdout).and_return(99)
expect(Process).to receive(:detach).with(99)
context 'when in child process' do
before do
# This signals the process that it's "inside" the fork
expect(Process).to receive(:fork).and_return(nil)
expect(Process).not_to receive(:detach)
end
described_class.spawn('sidekiq')
it 'starts the metrics server with the given arguments' do
expect_next_instance_of(MetricsServer) do |server|
expect(server).to receive(:start)
end
described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics')
end
it 'resets signal handlers from parent process' do
expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics', trapped_signals: %i[A B])
end
end
end
describe '#start' do
let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) }
let(:exporter_double) { double('fake_exporter', start: true) }
let(:prometheus_client_double) { double(::Prometheus::Client) }
let(:prometheus_config) { ::Prometheus::Client::Configuration.new }
let(:prometheus_config) { ::Prometheus::Client.configuration }
let(:metrics_dir) { Dir.mktmpdir }
let(:settings_double) { double(:settings, sidekiq_exporter: {}) }
let!(:old_metrics_dir) { ::Prometheus::Client.configuration.multiprocess_files_dir }
let(:settings) { { "fake_exporter" => { "enabled" => true } } }
let!(:old_metrics_dir) { prometheus_config.multiprocess_files_dir }
subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
before do
stub_env('prometheus_multiproc_dir', metrics_dir)
stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class)
allow(exporter_class).to receive(:instance).with({}, synchronous: true).and_return(exporter_double)
allow(Settings).to receive(:monitoring).and_return(settings_double)
expect(exporter_class).to receive(:instance).with(settings['fake_exporter'], synchronous: true).and_return(exporter_double)
expect(Settings).to receive(:monitoring).and_return(settings)
end
after do
Gitlab::Metrics.reset_registry!
::Prometheus::CleanupMultiprocDirService.new.execute
Dir.rmdir(metrics_dir)
::Prometheus::Client.configuration.multiprocess_files_dir = old_metrics_dir
FileUtils.rm_rf(metrics_dir, secure: true)
prometheus_config.multiprocess_files_dir = old_metrics_dir
end
it 'configures ::Prometheus::Client' do
allow(prometheus_client_double).to receive(:configuration).and_return(prometheus_config)
metrics_server.start
expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
@ -90,12 +105,5 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
metrics_server.start
end
it 'sends the correct Settings to the exporter instance' do
expect(Settings).to receive(:monitoring).and_return(settings_double)
expect(settings_double).to receive(:sidekiq_exporter)
metrics_server.start
end
end
end

View File

@ -5421,4 +5421,9 @@ RSpec.describe Ci::Build do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :ci_build }
end
it_behaves_like 'cleanup by a loose foreign key' do
let!(:model) { create(:ci_build, user: create(:user)) }
let!(:parent) { model.user }
end
end

View File

@ -700,4 +700,8 @@ RSpec.describe Ci::JobArtifact do
when changes or new entries are made.
MSG
end
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :ci_job_artifact }
end
end

View File

@ -4672,4 +4672,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :ci_pipeline }
end
it_behaves_like 'cleanup by a loose foreign key' do
let!(:model) { create(:ci_pipeline, user: create(:user)) }
let!(:parent) { model.user }
end
end

View File

@ -232,4 +232,8 @@ RSpec.describe ExternalPullRequest do
'with space/README.md']
end
end
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :external_pull_request }
end
end

View File

@ -5042,4 +5042,8 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(described_class.from_fork).to eq([fork_mr])
end
end
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :merge_request }
end
end

View File

@ -110,8 +110,8 @@ RSpec.describe User do
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
@ -1616,6 +1616,46 @@ RSpec.describe User do
end
end
describe 'enabled_static_object_token' do
let_it_be(:static_object_token) { 'ilqx6jm1u945macft4eff0nw' }
it 'returns incoming email token when supported' do
allow(Gitlab::CurrentSettings).to receive(:static_objects_external_storage_enabled?).and_return(true)
user = create(:user, static_object_token: static_object_token)
expect(user.enabled_static_object_token).to eq(static_object_token)
end
it 'returns `nil` when not supported' do
allow(Gitlab::CurrentSettings).to receive(:static_objects_external_storage_enabled?).and_return(false)
user = create(:user, static_object_token: static_object_token)
expect(user.enabled_static_object_token).to be_nil
end
end
describe 'enabled_incoming_email_token' do
let_it_be(:incoming_email_token) { 'ilqx6jm1u945macft4eff0nw' }
it 'returns incoming email token when supported' do
allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(true)
user = create(:user, incoming_email_token: incoming_email_token)
expect(user.enabled_incoming_email_token).to eq(incoming_email_token)
end
it 'returns `nil` when not supported' do
allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(false)
user = create(:user, incoming_email_token: incoming_email_token)
expect(user.enabled_incoming_email_token).to be_nil
end
end
describe '#recently_sent_password_reset?' do
it 'is false when reset_password_sent_at is nil' do
user = build_stubbed(:user, reset_password_sent_at: nil)
@ -6289,4 +6329,8 @@ RSpec.describe User do
expect(user.user_readme).to be(nil)
end
end
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :user }
end
end

View File

@ -0,0 +1,101 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::Crm::ContactsController do
let_it_be(:user) { create(:user) }
shared_examples 'response with 404 status' do
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples 'ok response with index template' do
it 'renders the index template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
shared_examples 'ok response with index template if authorized' do
context 'private group' do
let(:group) { create(:group, :private) }
context 'with authorized user' do
before do
group.add_reporter(user)
sign_in(user)
end
context 'when feature flag is enabled' do
it_behaves_like 'ok response with index template'
end
context 'when feature flag is not enabled' do
before do
stub_feature_flags(customer_relations: false)
end
it_behaves_like 'response with 404 status'
end
end
context 'with unauthorized user' do
before do
sign_in(user)
end
it_behaves_like 'response with 404 status'
end
context 'with anonymous user' do
it 'blah' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'public group' do
let(:group) { create(:group, :public) }
context 'with anonymous user' do
it_behaves_like 'ok response with index template'
end
end
end
describe 'GET #index' do
subject do
get group_crm_contacts_path(group)
response
end
it_behaves_like 'ok response with index template if authorized'
end
describe 'GET #new' do
subject do
get new_group_crm_contact_path(group)
response
end
it_behaves_like 'ok response with index template if authorized'
end
describe 'GET #edit' do
subject do
get edit_group_crm_contact_path(group, id: 1)
response
end
it_behaves_like 'ok response with index template if authorized'
end
end

View File

@ -213,6 +213,12 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) }
end
context 'with offset and limit' do
subject { described_class.where(id: [group_1, group_2]).offset(1).limit(1).self_and_descendants }
it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) }
end
end
describe '.self_and_descendants' do
@ -242,6 +248,19 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) }
end
context 'with offset and limit' do
subject do
described_class
.where(id: [group_1, group_2])
.limit(1)
.offset(1)
.self_and_descendant_ids
.pluck(:id)
end
it { is_expected.to contain_exactly(group_2.id, nested_group_2.id, deep_nested_group_2.id) }
end
end
describe '.self_and_descendant_ids' do

View File

@ -914,27 +914,28 @@
stylelint-declaration-strict-value "1.7.7"
stylelint-scss "3.18.0"
"@gitlab/svgs@1.226.0":
version "1.226.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.226.0.tgz#f590545df4bf871a9653f2ac93029fbc1bfd2788"
integrity sha512-oSPbwkPJ8yDttTy+zcqtA9TIPOGiTUXlgIf1XnlrMHUoQmzUUqkJMql6LDcP4xAqX0n+7Kinoxl8gmMSwMKYjw==
"@gitlab/svgs@1.229.0":
version "1.229.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.229.0.tgz#1ee863320ea3e0ff6b670dac59373b8b49e31388"
integrity sha512-10OLT3gj9AQ5DmcqaRcblkIY1dwr0danjaKl+hzjcA9sjvGuTNn3P/rQZglFanM2eI6MkoHG1YP7UeSs7cFuCQ==
"@gitlab/tributejs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@32.43.2":
version "32.43.2"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.43.2.tgz#df1e86493354db61da60e2eb8e9e67c79e9c402c"
integrity sha512-BJZJeXqi9MrJ5xAQ8rA2t95udSxsRGPeeCzlb6HI26j8LAJpVj9ArbiehoMZ45aGpX0+gnElMUGNOcZ8XHlQqw==
"@gitlab/ui@32.49.0":
version "32.49.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.49.0.tgz#d899e2e2487bb0e23a408386acc04eac14808de5"
integrity sha512-QB1M1/8vc1o0hAm5tg8tWIEcj5Isy2JxHFWKtDNnFqPvbb0QNBsoEazz9DNra3dNSRzt8zF8NJPqmuRT8WAvQA==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "2.20.1"
copy-to-clipboard "^3.0.8"
dompurify "^2.3.3"
dompurify "^2.3.4"
echarts "^5.2.1"
highlight.js "^10.6.0"
iframe-resizer "^4.3.2"
js-beautify "^1.8.8"
lodash "^4.17.20"
portal-vue "^2.1.6"
@ -3789,10 +3790,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@^3.19.3:
version "3.19.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.3.tgz#6df8142a996337503019ff3235a7022d7cdf4559"
integrity sha512-LeLBMgEGSsG7giquSzvgBrTS7V5UL6ks3eQlUSbN8dJStlLFiRzUm5iqsRyzUB8carhfKjkJ2vzKqE6z1Vga9g==
core-js@^3.20.0:
version "3.20.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.0.tgz#1c5ac07986b8d15473ab192e45a2e115a4a95b79"
integrity sha512-KjbKU7UEfg4YPpskMtMXPhUKn7m/1OdTHTVjy09ScR2LVaoUXe8Jh0UdvN2EKUR6iKTJph52SJP95mAB0MnVLQ==
core-js@~2.3.0:
version "2.3.0"
@ -4923,7 +4924,7 @@ dompurify@2.3.3:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c"
integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==
dompurify@^2.3.3, dompurify@^2.3.4:
dompurify@^2.3.4:
version "2.3.4"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.4.tgz#1cf5cf0105ccb4debdf6db162525bd41e6ddacc6"
integrity sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ==
@ -5041,7 +5042,12 @@ emittery@^0.7.1:
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451"
integrity sha512-d34LN4L6h18Bzz9xpoku2nPwKxCPlPMr3EEKTkoEBi+1/+b0lcRkRJ1UVyyZaKNeqGR3swcGl6s390DNO4YVgQ==
emoji-regex@^7.0.1, emoji-regex@^7.0.3:
emoji-regex@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.0.tgz#96559e19f82231b436403e059571241d627c42b8"
integrity sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
@ -6613,6 +6619,11 @@ iferr@^0.1.5:
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
iframe-resizer@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/iframe-resizer/-/iframe-resizer-4.3.2.tgz#42dd88345d18b9e377b6044dddb98c664ab0ce6b"
integrity sha512-gOWo2hmdPjMQsQ+zTKbses08mDfDEMh4NneGQNP4qwePYujY1lguqP6gnbeJkf154gojWlBhIltlgnMfYjGHWA==
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"