Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-08 12:08:46 +00:00
parent f4ea1f8998
commit cdda3d117c
96 changed files with 2853 additions and 1736 deletions

View File

@ -59,7 +59,7 @@ workflow:
variables:
PG_VERSION: "12"
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-2.7.patched-golang-1.17-node-16.14-postgresql-${PG_VERSION}:git-2.36-lfs-2.9-chrome-101-yarn-1.22-graphicsmagick-1.3.36"
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-2.7.patched-golang-1.17-node-16.14-postgresql-${PG_VERSION}:git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-yarn-1.22-graphicsmagick-1.3.36"
RAILS_ENV: "test"
NODE_ENV: "test"
BUNDLE_WITHOUT: "production:development"
@ -73,6 +73,8 @@ variables:
GIT_SUBMODULE_STRATEGY: "none"
GET_SOURCES_ATTEMPTS: "3"
DEBIAN_VERSION: "bullseye"
CHROME_VERSION: "101"
DOCKER_VERSION: "20.10.14"
TMP_TEST_FOLDER: "${CI_PROJECT_DIR}/tmp/tests"
GITLAB_WORKHORSE_FOLDER: "gitlab-workhorse"
@ -89,7 +91,6 @@ variables:
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
DOCKER_VERSION: "20.10.1"
CACHE_CLASSES: "true"
CHECK_PRECOMPILED_ASSETS: "true"
FF_USE_FASTZIP: "true"

View File

@ -29,7 +29,15 @@ build-qa-image:
- !reference [.base-image-build, script]
- echo $QA_IMAGE
- echo $QA_IMAGE_BRANCH
- /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --destination=${QA_IMAGE_BRANCH} --cache=true
- |
/kaniko/executor \
--context=${CI_PROJECT_DIR} \
--dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile \
--destination=${QA_IMAGE} \
--destination=${QA_IMAGE_BRANCH} \
--build-arg=CHROME_VERSION=${CHROME_VERSION} \
--build-arg=DOCKER_VERSION=${DOCKER_VERSION} \
--cache=true
# This image is used by:
# - The `CNG` pipelines (via the `review-build-cng` job): https://gitlab.com/gitlab-org/build/CNG/-/blob/cfc67136d711e1c8c409bf8e57427a644393da2f/.gitlab-ci.yml#L335

View File

@ -1,5 +1,5 @@
.qa-job-base:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-99
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-${CHROME_VERSION}
extends:
- .default-retry
- .qa-cache
@ -12,7 +12,7 @@
before_script:
- !reference [.default-before_script, before_script]
- cd qa/
- bundle_install_script
- bundle install
qa:internal:
extends:

View File

@ -1,6 +1,6 @@
include:
- project: gitlab-org/quality/pipeline-common
ref: 0.6.0
ref: 0.13.0
file:
- /ci/allure-report.yml
- /ci/knapsack-report.yml
@ -28,7 +28,7 @@ include:
- .qa-cache
- .test_variables
- .bundler_variables
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-99-docker-20.10.14-gcloud-383-kubectl-1.23
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23
stage: qa
needs:
- review-deploy
@ -81,7 +81,7 @@ include:
# Store knapsack report as artifact so the same report is reused across all jobs
download-knapsack-report:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-99
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-${CHROME_VERSION}
extends:
- .qa-cache
- .bundler_variables

View File

@ -42,6 +42,20 @@ const bodyTrClass =
export default {
i18n,
typeSet,
modal: {
actionPrimary: {
text: i18n.deleteIntegration,
attributes: {
variant: 'danger',
},
},
actionSecondary: {
text: __('Cancel'),
attributes: {
variant: 'default',
},
},
},
components: {
GlButtonGroup,
GlButton,
@ -204,8 +218,8 @@ export default {
<gl-modal
modal-id="deleteIntegration"
:title="$options.i18n.deleteIntegration"
:ok-title="$options.i18n.deleteIntegration"
ok-variant="danger"
:action-primary="$options.modal.actionPrimary"
:action-secondary="$options.modal.actionSecondary"
@ok="deleteIntegration"
>
<gl-sprintf

View File

@ -374,6 +374,33 @@
"type": "array",
"description": "Command or script that should be executed as the container's entrypoint. It will be translated to Docker's --entrypoint option while creating the container. The syntax is similar to Dockerfile's ENTRYPOINT directive, where each shell token is a separate string in the array.",
"minItems": 1
},
"pull_policy": {
"markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#imagepull_policy).",
"default": "always",
"oneOf": [
{
"type": "string",
"enum": [
"always",
"never",
"if-not-present"
]
},
{
"type": "array",
"items": {
"type": "string",
"enum": [
"always",
"never",
"if-not-present"
]
},
"minItems": 1,
"uniqueItems": true
}
]
}
},
"required": ["name"]

View File

@ -0,0 +1,16 @@
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
const GROUP_SUBGROUPS_PATH = '/-/autocomplete/group_subgroups.json';
const buildUrl = (urlRoot, url) => {
return joinPaths(urlRoot, url);
};
export const getSubGroups = () => {
return axios.get(buildUrl(gon.relative_url_root || '', GROUP_SUBGROUPS_PATH), {
params: {
group_id: gon.current_group_id,
},
});
};

View File

@ -0,0 +1,194 @@
<script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
import createFlash from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getSubGroups } from '../api/access_dropdown_api';
import { LEVEL_TYPES } from '../constants';
export const i18n = {
selectUsers: s__('ProtectedEnvironment|Select groups'),
groupsSectionHeader: s__('AccessDropdown|Groups'),
};
export default {
i18n,
components: {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
},
props: {
hasLicense: {
required: false,
type: Boolean,
default: true,
},
label: {
type: String,
required: false,
default: i18n.selectUsers,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
preselectedItems: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
loading: false,
initialLoading: false,
query: '',
groups: [],
selected: {
[LEVEL_TYPES.GROUP]: [],
},
};
},
computed: {
preselected() {
return groupBy(this.preselectedItems, 'type');
},
toggleLabel() {
const counts = Object.fromEntries(
Object.entries(this.selected).map(([key, value]) => [key, value.length]),
);
const labelPieces = [];
if (counts[LEVEL_TYPES.GROUP] > 0) {
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
}
return labelPieces.join(', ') || this.label;
},
toggleClass() {
return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
},
selection() {
return [...this.getDataForSave(LEVEL_TYPES.GROUP, 'group_id')];
},
},
watch: {
query: debounce(function debouncedSearch() {
return this.getData();
}, 500),
},
created() {
this.getData({ initial: true });
},
methods: {
focusInput() {
this.$refs.search.focusInput();
},
getData({ initial = false } = {}) {
this.initialLoading = initial;
this.loading = true;
if (this.hasLicense) {
Promise.all([this.groups.length ? Promise.resolve({ data: this.groups }) : getSubGroups()])
.then(([groupsResponse]) => {
this.consolidateData(groupsResponse.data);
this.setSelected({ initial });
})
.catch(() => createFlash({ message: __('Failed to load groups.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;
});
}
},
consolidateData(groupsResponse = []) {
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
}
},
setSelected({ initial } = {}) {
if (initial) {
const selectedGroups = intersectionWith(
this.groups,
this.preselectedItems,
(group, selected) => {
return selected.type === LEVEL_TYPES.GROUP && group.id === selected.group_id;
},
);
this.selected[LEVEL_TYPES.GROUP] = selectedGroups;
}
},
getDataForSave(accessType, key) {
const selected = this.selected[accessType].map(({ id }) => ({ [key]: id }));
const preselected = this.preselected[accessType];
const added = differenceBy(selected, preselected, key);
const preserved = intersectionBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
id,
[key]: keyId,
}));
const removed = differenceBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
id,
[key]: keyId,
_destroy: true,
}));
return [...added, ...removed, ...preserved];
},
onItemClick(item) {
this.toggleSelection(this.selected[item.type], item);
this.emitUpdate();
},
toggleSelection(arr, item) {
const itemIndex = arr.findIndex(({ id }) => id === item.id);
if (itemIndex > -1) {
arr.splice(itemIndex, 1);
} else arr.push(item);
},
isSelected(item) {
return this.selected[item.type].some((selected) => selected.id === item.id);
},
emitUpdate() {
this.$emit('select', this.selection);
},
onHide() {
this.$emit('hidden', this.selection);
},
},
};
</script>
<template>
<gl-dropdown
:disabled="disabled || initialLoading"
:text="toggleLabel"
class="gl-min-w-20"
:toggle-class="toggleClass"
aria-labelledby="allowed-users-label"
@shown="focusInput"
@hidden="onHide"
>
<template #header>
<gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
</template>
<template v-if="groups.length">
<gl-dropdown-section-header>{{
$options.i18n.groupsSectionHeader
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="group in groups"
:key="`${group.id}${group.name}`"
fingerprint
data-testid="group-dropdown-item"
:avatar-url="group.avatar_url"
is-check-item
:is-checked="isSelected(group)"
@click.native.capture.stop="onItemClick(group)"
>
{{ group.name }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>

View File

@ -0,0 +1,3 @@
export const LEVEL_TYPES = {
GROUP: 'group',
};

View File

@ -0,0 +1,36 @@
import * as Sentry from '@sentry/browser';
import Vue from 'vue';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el) => {
if (!el) {
return false;
}
const { label, disabled, preselectedItems } = el.dataset;
let preselected = [];
try {
preselected = JSON.parse(preselectedItems);
} catch (e) {
Sentry.captureException(e);
}
return new Vue({
el,
render(createElement) {
const vm = this;
return createElement(AccessDropdown, {
props: {
preselectedItems: preselected,
label,
disabled,
},
on: {
select(selected) {
vm.$emit('select', selected);
},
},
});
},
});
};

View File

@ -14,6 +14,7 @@ import ExperimentTracking from '~/experimentation/experiment_tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
import {
CLOSE_TO_LIMIT_COUNT,
USERS_FILTER_ALL,
INVITE_MEMBERS_FOR_TASK,
MEMBER_MODAL_LABELS,
@ -151,6 +152,16 @@ export default {
isOnLearnGitlab() {
return this.source === LEARN_GITLAB;
},
closeToLimit() {
if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
return (
this.usersLimitDataset.membersCount >=
this.usersLimitDataset.freeUsersLimit - CLOSE_TO_LIMIT_COUNT
);
}
return false;
},
reachedLimit() {
if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
return this.usersLimitDataset.membersCount >= this.usersLimitDataset.freeUsersLimit;
@ -297,6 +308,7 @@ export default {
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
:close-to-limit="closeToLimit"
:reached-limit="reachedLimit"
:users-limit-dataset="usersLimitDataset"
@reset="resetFields"
@ -314,6 +326,7 @@ export default {
<template #user-limit-notification>
<user-limit-notification
:close-to-limit="closeToLimit"
:reached-limit="reachedLimit"
:users-limit-dataset="usersLimitDataset"
/>

View File

@ -131,6 +131,11 @@ export default {
required: false,
default: false,
},
closeToLimit: {
type: Boolean,
required: false,
default: false,
},
reachedLimit: {
type: Boolean,
required: false,
@ -183,6 +188,17 @@ export default {
actionCancel() {
if (this.reachedLimit && this.usersLimitDataset.userNamespace) return undefined;
if (this.closeToLimit && this.usersLimitDataset.userNamespace) {
return {
text: INVITE_BUTTON_TEXT_DISABLED,
attributes: {
href: this.usersLimitDataset.membersPath,
category: 'secondary',
variant: 'confirm',
},
};
}
return {
text: this.reachedLimit ? CANCEL_BUTTON_TEXT_DISABLED : this.cancelButtonText,
...(this.reachedLimit && { attributes: { href: this.usersLimitDataset.purchasePath } }),

View File

@ -8,15 +8,20 @@ import {
REACHED_LIMIT_MESSAGE,
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
CLOSE_TO_LIMIT_MESSAGE,
CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
DANGER_ALERT_TITLE_PERSONAL_NAMESPACE,
WARNING_ALERT_TITLE_PERSONAL_NAMESPACE,
} from '../constants';
const CLOSE_TO_LIMIT_COUNT = 2;
export default {
name: 'UserLimitNotification',
components: { GlAlert, GlSprintf, GlLink },
inject: ['name'],
props: {
closeToLimit: {
type: Boolean,
required: true,
},
reachedLimit: {
type: Boolean,
required: true,
@ -40,14 +45,14 @@ export default {
purchasePath() {
return this.usersLimitDataset.purchasePath;
},
closeToLimit() {
if (this.freeUsersLimit && this.membersCount) {
return this.membersCount >= this.freeUsersLimit - CLOSE_TO_LIMIT_COUNT;
warningAlertTitle() {
if (this.usersLimitDataset.userNamespace) {
return sprintf(WARNING_ALERT_TITLE_PERSONAL_NAMESPACE, {
count: this.freeUsersLimit - this.membersCount,
members: this.pluralMembers(this.freeUsersLimit - this.membersCount),
});
}
return false;
},
warningAlertTitle() {
return sprintf(WARNING_ALERT_TITLE, {
count: this.freeUsersLimit - this.membersCount,
members: this.pluralMembers(this.freeUsersLimit - this.membersCount),
@ -55,6 +60,13 @@ export default {
});
},
dangerAlertTitle() {
if (this.usersLimitDataset.userNamespace) {
return sprintf(DANGER_ALERT_TITLE_PERSONAL_NAMESPACE, {
count: this.freeUsersLimit,
members: this.pluralMembers(this.freeUsersLimit),
});
}
return sprintf(DANGER_ALERT_TITLE, {
count: this.freeUsersLimit,
members: this.pluralMembers(this.freeUsersLimit),
@ -79,6 +91,10 @@ export default {
return this.reachedLimitMessage;
}
if (this.usersLimitDataset.userNamespace) {
return this.$options.i18n.closeToLimitMessagePersonalNamespace;
}
return this.$options.i18n.closeToLimitMessage;
},
},
@ -91,6 +107,7 @@ export default {
reachedLimitMessage: REACHED_LIMIT_MESSAGE,
reachedLimitUpgradeSuggestionMessage: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
closeToLimitMessage: CLOSE_TO_LIMIT_MESSAGE,
closeToLimitMessagePersonalNamespace: CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
},
};
</script>

View File

@ -1,7 +1,7 @@
import { s__ } from '~/locale';
export const CLOSE_TO_LIMIT_COUNT = 2;
export const SEARCH_DELAY = 200;
export const INVITE_MEMBERS_FOR_TASK = {
minimum_access_level: 30,
name: 'invite_members_for_task',
@ -132,10 +132,17 @@ export const ON_SUBMIT_TRACK_LABEL = 'manage_members_clicked';
export const WARNING_ALERT_TITLE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
);
export const WARNING_ALERT_TITLE_PERSONAL_NAMESPACE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in your personal projects',
);
export const DANGER_ALERT_TITLE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
);
export const DANGER_ALERT_TITLE_PERSONAL_NAMESPACE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for your personal projects",
);
export const REACHED_LIMIT_MESSAGE = s__(
'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access.',
);
@ -149,3 +156,6 @@ export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.co
export const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);
export const CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE = s__(
'InviteMembersModal|To make more space, you can remove members who no longer need access.',
);

View File

@ -10,7 +10,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-new-user-signups-cap-reached',
'.js-eoa-bronze-plan-banner',
'.js-security-newsletter-callout',
'.js-approaching-seats-count-threshold',
'.js-approaching-seat-count-threshold',
'.js-storage-enforcement-banner',
'.js-user-over-limit-free-plan-alert',
'.js-minute-limit-banner',

View File

@ -103,9 +103,9 @@ export default {
},
expandedIcon() {
if (this.isUpstream) {
return this.expanded ? 'angle-right' : 'angle-left';
return this.expanded ? 'chevron-lg-right' : 'chevron-lg-left';
}
return this.expanded ? 'angle-left' : 'angle-right';
return this.expanded ? 'chevron-lg-left' : 'chevron-lg-right';
},
expandBtnText() {
return this.expanded ? __('Collapse jobs') : __('Expand jobs');

View File

@ -126,6 +126,7 @@ export default {
}) => {
toast(__('Marked as ready. Merging is now allowed.'));
$('.merge-request .detail-page-description .title').text(title);
eventHub.$emit('MRWidgetUpdateRequested');
},
)
.catch(() =>

View File

@ -27,7 +27,8 @@
}
.toggle-sidebar-button {
width: $contextual-sidebar-collapsed-width;
width: #{$contextual-sidebar-collapsed-width - 1px};
padding: 0 21px;
.collapse-text {
display: none;
@ -81,7 +82,7 @@
@include gl-px-0;
@include gl-pb-2;
@include gl-pt-0;
background-color: $gray-10;
@include gl-bg-gray-10;
box-shadow: 0 $gl-spacing-scale-2 $gl-spacing-scale-5 $t-gray-a-24, 0 0 $gl-spacing-scale-1 $t-gray-a-24;
border-style: none;
border-radius: $border-radius-default;
@ -128,7 +129,7 @@
@include gl-p-2;
@include gl-mb-2;
@include gl-mt-0;
@include gl-mt-1;
.avatar-container {
@include gl-font-weight-normal;
@ -246,7 +247,8 @@
z-index: 600;
width: $contextual-sidebar-width;
top: $header-height;
background-color: $gray-50;
@include gl-bg-gray-10;
border-right: 1px solid $gray-50;
transform: translate3d(0, 0, 0);
&.sidebar-collapsed-desktop {
@ -352,7 +354,6 @@
}
.sidebar-top-level-items {
@include gl-mt-2;
margin-bottom: 60px;
.context-header a {
@ -410,11 +411,10 @@
.toggle-sidebar-button,
.close-nav-button {
@include side-panel-toggle;
background-color: $gray-50;
border-top: 1px solid $border-color;
@include gl-bg-gray-10;
position: fixed;
bottom: 0;
width: $contextual-sidebar-width;
width: #{$contextual-sidebar-width - 1px};
.collapse-text,
.icon-chevron-double-lg-left,

View File

@ -52,7 +52,7 @@
display: flex;
align-items: center;
padding: 2px 8px;
margin: 4px 2px 4px -12px;
margin: 4px 2px 4px -8px;
border-radius: $border-radius-default;
&:active,

View File

@ -592,6 +592,14 @@
@include email-code-block;
}
/**
* Links
*
*/
a:focus-visible {
@include gl-focus($outline: true, $outline-offset: $outline-width);
}
/**
* Headers
*

View File

@ -9,7 +9,7 @@ $sidebar-transition-duration: 0.3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 256px;
$contextual-sidebar-collapsed-width: 48px;
$contextual-sidebar-collapsed-width: 56px;
$toggle-sidebar-height: 48px;
/**
@ -580,7 +580,7 @@ $sidebar-toggle-height: 60px;
$sidebar-toggle-width: 40px;
$sidebar-milestone-toggle-bottom-margin: 10px;
$sidebar-avatar-size: 32px;
$sidebar-top-item-lr-margin: 4px;
$sidebar-top-item-lr-margin: 8px;
$sidebar-top-item-tb-margin: 1px;
/*

View File

@ -5,14 +5,6 @@
line-height: 34px;
display: flex;
a {
color: $gl-text-color;
&.link {
color: $blue-600;
}
}
.author-link {
white-space: nowrap;
}
@ -22,6 +14,15 @@
}
}
.detail-page-header a {
color: $gl-text-color;
}
.detail-page-header a.link,
.detail-page-header .title a {
color: $blue-600;
}
.detail-page-header-body {
position: relative;
display: flex;

View File

@ -214,8 +214,7 @@ $tabs-holder-z-index: 250;
}
}
.merge-request-tabs-holder,
.epic-tabs-holder {
.merge-request-tabs-holder {
top: $header-height;
z-index: $tabs-holder-z-index;
background-color: $body-bg;
@ -248,14 +247,12 @@ $tabs-holder-z-index: 250;
}
.with-performance-bar {
.merge-request-tabs-holder,
.epic-tabs-holder {
.merge-request-tabs-holder {
top: calc(#{$header-height} + #{$performance-bar-height});
}
}
.merge-request-tabs,
.epic-tabs {
.merge-request-tabs {
display: flex;
flex-wrap: nowrap;
margin-bottom: 0;
@ -263,8 +260,7 @@ $tabs-holder-z-index: 250;
}
.limit-container-width {
.merge-request-tabs-container,
.epic-tabs-container {
.merge-request-tabs-container {
max-width: $limited-layout-width;
margin-left: auto;
margin-right: auto;
@ -277,8 +273,7 @@ $tabs-holder-z-index: 250;
}
}
.merge-request-tabs-container,
.epic-tabs-container {
.merge-request-tabs-container {
display: flex;
justify-content: space-between;
@ -308,16 +303,14 @@ $tabs-holder-z-index: 250;
// Wrap MR tabs/buttons so you don't have to scroll on desktop
@include media-breakpoint-down(md) {
.merge-request-tabs-container,
.epic-tabs-container {
.merge-request-tabs-container {
flex-direction: column-reverse;
}
}
@include media-breakpoint-down(lg) {
.right-sidebar-expanded {
.merge-request-tabs-container,
.epic-tabs-container {
.merge-request-tabs-container {
flex-direction: column-reverse;
align-items: flex-start;
}

View File

@ -780,7 +780,7 @@ input {
display: flex;
align-items: center;
padding: 2px 8px;
margin: 4px 2px 4px -12px;
margin: 4px 2px 4px -8px;
border-radius: 4px;
}
.navbar-gitlab .header-content .title a:active {
@ -1014,7 +1014,7 @@ input {
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
padding-left: 48px;
padding-left: 56px;
}
}
@media (min-width: 1200px) {
@ -1024,7 +1024,7 @@ input {
}
@media (min-width: 768px) {
.page-with-icon-sidebar {
padding-left: 48px;
padding-left: 56px;
}
}
.nav-sidebar {
@ -1034,11 +1034,12 @@ input {
z-index: 600;
width: 256px;
top: var(--header-height, 48px);
background-color: #303030;
background-color: #1f1f1f;
border-right: 1px solid #303030;
transform: translate3d(0, 0, 0);
}
.nav-sidebar.sidebar-collapsed-desktop {
width: 48px;
width: 56px;
}
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1091,7 +1092,7 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
margin: 1px 4px;
margin: 1px 8px;
}
.nav-sidebar li.active > a {
font-weight: 600;
@ -1227,7 +1228,7 @@ input {
}
@media (min-width: 768px) and (max-width: 1199px) {
.nav-sidebar:not(.sidebar-expanded-mobile) {
width: 48px;
width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1262,7 +1263,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
height: 60px;
width: 48px;
width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
padding: 10px 4px;
@ -1294,7 +1295,8 @@ input {
margin-right: 0;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
width: 48px;
width: 55px;
padding: 0 21px;
}
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
@ -1327,10 +1329,10 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
margin: 1px 4px;
margin: 1px 8px;
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
margin-top: 0.125rem;
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
@ -1349,13 +1351,12 @@ input {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
margin-top: 0.25rem;
margin-bottom: 60px;
}
.sidebar-top-level-items .context-header a {
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
margin-top: 0.125rem;
}
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
@ -1401,11 +1402,10 @@ input {
color: #999;
display: flex;
align-items: center;
background-color: #303030;
border-top: 1px solid #404040;
background-color: #1f1f1f;
position: fixed;
bottom: 0;
width: 256px;
width: 255px;
}
.toggle-sidebar-button .collapse-text,
.toggle-sidebar-button .icon-chevron-double-lg-left,
@ -1419,7 +1419,7 @@ input {
}
.sidebar-collapsed-desktop .context-header {
height: 60px;
width: 48px;
width: 56px;
}
.sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@ -1451,7 +1451,8 @@ input {
margin-right: 0;
}
.sidebar-collapsed-desktop .toggle-sidebar-button {
width: 48px;
width: 55px;
padding: 0 21px;
}
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
display: none;
@ -1787,6 +1788,11 @@ body.gl-dark {
--svg-status-bg: #333;
--nav-active-bg: rgba(255, 255, 255, 0.08);
}
.nav-sidebar,
.toggle-sidebar-button,
.close-nav-button {
background-color: #262626;
}
.nav-sidebar li a {
color: var(--gray-600);
}

View File

@ -765,7 +765,7 @@ input {
display: flex;
align-items: center;
padding: 2px 8px;
margin: 4px 2px 4px -12px;
margin: 4px 2px 4px -8px;
border-radius: 4px;
}
.navbar-gitlab .header-content .title a:active {
@ -999,7 +999,7 @@ input {
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
padding-left: 48px;
padding-left: 56px;
}
}
@media (min-width: 1200px) {
@ -1009,7 +1009,7 @@ input {
}
@media (min-width: 768px) {
.page-with-icon-sidebar {
padding-left: 48px;
padding-left: 56px;
}
}
.nav-sidebar {
@ -1019,11 +1019,12 @@ input {
z-index: 600;
width: 256px;
top: var(--header-height, 48px);
background-color: #f0f0f0;
background-color: #fafafa;
border-right: 1px solid #f0f0f0;
transform: translate3d(0, 0, 0);
}
.nav-sidebar.sidebar-collapsed-desktop {
width: 48px;
width: 56px;
}
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1076,7 +1077,7 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
margin: 1px 4px;
margin: 1px 8px;
}
.nav-sidebar li.active > a {
font-weight: 600;
@ -1212,7 +1213,7 @@ input {
}
@media (min-width: 768px) and (max-width: 1199px) {
.nav-sidebar:not(.sidebar-expanded-mobile) {
width: 48px;
width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
overflow-x: hidden;
@ -1247,7 +1248,7 @@ input {
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
height: 60px;
width: 48px;
width: 56px;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
padding: 10px 4px;
@ -1279,7 +1280,8 @@ input {
margin-right: 0;
}
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
width: 48px;
width: 55px;
padding: 0 21px;
}
.nav-sidebar:not(.sidebar-expanded-mobile)
.toggle-sidebar-button
@ -1312,10 +1314,10 @@ input {
border-radius: 0.25rem;
width: auto;
line-height: 1rem;
margin: 1px 4px;
margin: 1px 8px;
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
margin-top: 0.125rem;
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
@ -1334,13 +1336,12 @@ input {
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
margin-top: 0.25rem;
margin-bottom: 60px;
}
.sidebar-top-level-items .context-header a {
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0;
margin-top: 0.125rem;
}
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
@ -1386,11 +1387,10 @@ input {
color: #666;
display: flex;
align-items: center;
background-color: #f0f0f0;
border-top: 1px solid #dbdbdb;
background-color: #fafafa;
position: fixed;
bottom: 0;
width: 256px;
width: 255px;
}
.toggle-sidebar-button .collapse-text,
.toggle-sidebar-button .icon-chevron-double-lg-left,
@ -1404,7 +1404,7 @@ input {
}
.sidebar-collapsed-desktop .context-header {
height: 60px;
width: 48px;
width: 56px;
}
.sidebar-collapsed-desktop .context-header a {
padding: 10px 4px;
@ -1436,7 +1436,8 @@ input {
margin-right: 0;
}
.sidebar-collapsed-desktop .toggle-sidebar-button {
width: 48px;
width: 55px;
padding: 0 21px;
}
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
display: none;

View File

@ -41,6 +41,12 @@
border-color: $gray-800;
}
.nav-sidebar,
.toggle-sidebar-button,
.close-nav-button {
background-color: darken($gray-50, 4%);
}
.nav-sidebar {
li {
a {
@ -68,6 +74,11 @@
}
}
aside.right-sidebar:not(.right-sidebar-merge-requests) {
background-color: $gray-10;
border-left-color: $gray-50;
}
body.gl-dark {
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $gray-900, $white);

View File

@ -9,6 +9,7 @@ module Groups
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action :define_variables, only: [:show]
before_action :push_licensed_features, only: [:show]
before_action :assign_variables_to_gon, only: [:show]
feature_category :continuous_integration
urgency :low
@ -81,6 +82,10 @@ module Groups
# Overridden in EE
def push_licensed_features
end
# Overridden in EE
def assign_variables_to_gon
end
end
end
end

View File

@ -142,7 +142,7 @@ class IssuableFinder
projects_public_or_visible_to_user
end
projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
projects.with_feature_available_for_user(klass.base_class, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
end
end
@ -215,7 +215,7 @@ class IssuableFinder
end
def min_access_level
ProjectFeature.required_minimum_access_level(klass)
ProjectFeature.required_minimum_access_level(klass.base_class)
end
def method_missing(method_name, *args, &block)

View File

@ -27,7 +27,7 @@ module Issuables
def by_label(issuables)
return issuables unless label_names_from_params.present?
target_model = issuables.model
target_model = issuables.base_class
if filter_by_no_label?
issuables.where(label_link_query(target_model).arel.exists.not)
@ -55,7 +55,7 @@ module Issuables
# rubocop: disable CodeReuse/ActiveRecord
def issuables_with_selected_labels(issuables, label_names)
target_model = issuables.model
target_model = issuables.base_class
if root_namespace
all_label_ids = find_label_ids(label_names)
@ -77,7 +77,7 @@ module Issuables
# rubocop: disable CodeReuse/ActiveRecord
def issuables_without_selected_labels(issuables, label_names)
target_model = issuables.model
target_model = issuables.base_class
if root_namespace
label_ids = find_label_ids(label_names).flatten(1)

View File

@ -37,7 +37,7 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def klass
Issue.includes(:author)
model_class.includes(:author)
end
# rubocop: enable CodeReuse/ActiveRecord
@ -47,10 +47,10 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def with_confidentiality_access_check
return Issue.all if params.user_can_see_all_issues?
return model_class.all if params.user_can_see_all_issues?
# Only admins can see hidden issues, so for non-admins, we filter out any hidden issues
issues = Issue.without_hidden
issues = model_class.without_hidden
return issues.all if params.user_can_see_all_confidential_issues?
@ -77,7 +77,7 @@ class IssuesFinder < IssuableFinder
def init_collection
if params.public_only?
Issue.public_only
model_class.public_only
else
with_confidentiality_access_check
end
@ -129,7 +129,7 @@ class IssuesFinder < IssuableFinder
def by_issue_types(items)
issue_type_params = Array(params[:issue_types]).map(&:to_s)
return items if issue_type_params.blank?
return Issue.none unless (WorkItems::Type.base_types.keys & issue_type_params).sort == issue_type_params.sort
return model_class.none unless (WorkItems::Type.base_types.keys & issue_type_params).sort == issue_type_params.sort
items.with_issue_type(params[:issue_types])
end
@ -140,6 +140,10 @@ class IssuesFinder < IssuableFinder
items.without_issue_type(issue_type_params)
end
def model_class
Issue
end
end
IssuesFinder.prepend_mod_with('IssuesFinder')

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
# WorkItem model inherits from Issue model. It's planned to be its extension
# with widgets support. Because WorkItems are internally Issues, WorkItemsFinder
# can be almost identical to IssuesFinder, except it should return instances of
# WorkItems instead of Issues
module WorkItems
class WorkItemsFinder < IssuesFinder
def params_class
::IssuesFinder::Params
end
private
def model_class
WorkItem
end
end
end

View File

@ -569,7 +569,7 @@ module Ci
end
def stop_action_successful?
Feature.disabled?(:env_stopped_on_stop_success, project) || success?
success?
end
##

View File

@ -18,7 +18,7 @@ module Awardable
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
.where(award_emoji_table[:awardable_type].eq(self.name))
.where(award_emoji_table[:awardable_type].eq(base_class.name))
.where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
@ -31,7 +31,7 @@ module Awardable
inner_query = award_emoji_table
.project('true')
.where(award_emoji_table[:user_id].eq(user.id))
.where(award_emoji_table[:awardable_type].eq(self.name))
.where(award_emoji_table[:awardable_type].eq(base_class.name))
.where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
@ -56,13 +56,11 @@ module Awardable
awardable_table = self.arel_table
awards_table = AwardEmoji.arel_table
join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
awards_table[:awardable_id].eq(awardable_table[:id]).and(
awards_table[:awardable_type].eq(self.name).and(
awards_table[:name].eq(emoji_name)
)
)
).join_sources
join_clause = awardable_table
.join(awards_table, Arel::Nodes::OuterJoin)
.on(awards_table[:awardable_id].eq(awardable_table[:id])
.and(awards_table[:awardable_type].eq(base_class.name).and(awards_table[:name].eq(emoji_name))))
.join_sources
joins(join_clause).group(awardable_table[:id]).reorder(
Arel.sql("COUNT(award_emoji.id) #{direction}")

View File

@ -106,23 +106,23 @@ module Issuable
scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection
# The `to_ability_name` method is not an user input.
# The `assignee_association_name` method is not an user input.
scope :assigned, -> do
where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
where("EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)")
end
scope :unassigned, -> do
where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
where("NOT EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)")
end
scope :assigned_to, ->(users) do
assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass
condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id"))
where(condition.arel.exists)
end
scope :not_assigned_to, ->(users) do
assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass
condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id"))
where(condition.arel.exists.not)
end
# rubocop:enable GitlabSecurity/SqlInjection
@ -412,6 +412,10 @@ module Issuable
def parent_class
::Project
end
def assignee_association_name
to_ability_name
end
end
def state

View File

@ -15,17 +15,29 @@ module Limitable
validate :validate_plan_limit_not_exceeded, on: :create
end
def exceeds_limits?
limits, relation = fetch_plan_limit_data
limits&.exceeded?(limit_name, relation)
end
private
def validate_plan_limit_not_exceeded
limits, relation = fetch_plan_limit_data
check_plan_limit_not_exceeded(limits, relation)
end
def fetch_plan_limit_data
if GLOBAL_SCOPE == limit_scope
validate_global_plan_limit_not_exceeded
global_plan_limits
else
validate_scoped_plan_limit_not_exceeded
scoped_plan_limits
end
end
def validate_scoped_plan_limit_not_exceeded
def scoped_plan_limits
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
return unless scope_relation
return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation)
@ -34,18 +46,18 @@ module Limitable
relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend
limits = scope_relation.actual_limits
check_plan_limit_not_exceeded(limits, relation)
[limits, relation]
end
def validate_global_plan_limit_not_exceeded
def global_plan_limits
relation = self.class.all
limits = Plan.default.actual_limits
check_plan_limit_not_exceeded(limits, relation)
[limits, relation]
end
def check_plan_limit_not_exceeded(limits, relation)
return unless limits.exceeded?(limit_name, relation)
return unless limits&.exceeded?(limit_name, relation)
errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
{ name: limit_name.humanize(capitalize: false), count: limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend

View File

@ -300,7 +300,7 @@ class Environment < ApplicationRecord
end
def wait_for_stop?
stop_actions.present? && Feature.enabled?(:env_stopped_on_stop_success, project)
stop_actions.present?
end
def stop_with_actions!(current_user)

View File

@ -13,6 +13,10 @@ class WorkItem < Issue
scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
def self.assignee_association_name
'issue'
end
def noteable_target_type_name
'issue'
end

View File

@ -669,6 +669,7 @@ class ProjectPolicy < BasePolicy
enable :read_design
enable :read_design_activity
enable :read_issue_link
enable :read_work_item
end
rule { can?(:developer_access) }.policy do

View File

@ -8,4 +8,9 @@ class WorkItemPolicy < IssuePolicy
rule { can?(:update_issue) }.enable :update_work_item
rule { can?(:read_issue) }.enable :read_work_item
# because IssuePolicy delegates to ProjectPolicy and
# :read_work_item is enabled in ProjectPolicy too, we
# need to make sure we also prevent this rule if read_issue
# is prevented
rule { ~can?(:read_issue) }.prevent :read_work_item
end

View File

@ -32,6 +32,8 @@ module BulkImports
destination_filepath = File.join(export_path, lfs_object.oid)
if lfs_object.local_store?
return unless File.exist?(lfs_object.file.path)
copy_files(lfs_object.file.path, destination_filepath)
else
download(lfs_object.file.url, destination_filepath)

View File

@ -22,7 +22,7 @@ module ResourceEvents
end
def build_resource_args
key = resource.class.name.foreign_key
key = resource.class.base_class.name.foreign_key
{
user_id: user.id,

View File

@ -1,22 +1,19 @@
%fieldset
%legend
%legend.gl-border-bottom-0
= s_('AdminUsers|Access')
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :projects_limit
.col-sm-10
= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control gl-form-input'
.form-group.row
.col-sm-2.col-form-label.gl-pt-0
.col-12.gl-pt-0
= f.label :can_create_group
.col-sm-10
= f.gitlab_ui_checkbox_component :can_create_group, ''
.form-group.row
.col-sm-2.col-form-label.gl-pt-0
.col-12.gl-pt-0
= f.label :access_level
.col-sm-10
- editing_current_user = (current_user == @user)
= f.gitlab_ui_radio_component :access_level, :regular,
@ -35,10 +32,10 @@
.form-group.row
.col-sm-2.col-form-label.gl-pt-0
.col-12.gl-pt-0
= f.label :external
.hidden{ data: user_internal_regex_data }
.col-sm-10.gl-display-flex.gl-align-items-baseline
.col-12.gl-display-flex.gl-align-items-baseline
= f.gitlab_ui_checkbox_component :external, s_('AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets.')
%row.hidden#warning_external_automatically_set
= gl_badge_tag s_('AdminUsers|Automatically marked as default internal user'), variant: :warning
@ -46,9 +43,9 @@
.form-group.row
- @user.credit_card_validation || @user.build_credit_card_validation
= f.fields_for :credit_card_validation do |ff|
.col-sm-2.col-form-label.gl-pt-0
.col-12.gl-pt-0
= ff.label s_('AdminUsers|Validate user account')
.col-sm-10.gl-display-flex.gl-align-items-baseline
.col-12.gl-display-flex.gl-align-items-baseline
= ff.gitlab_ui_checkbox_component :credit_card_validated_at,
s_('AdminUsers|User is validated and can use free CI minutes on shared runners.'),
help_text: s_('AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user.'),

View File

@ -1,7 +1,6 @@
%fieldset
%legend= _('Admin notes')
%legend.gl-border-bottom-0= _('Admin notes')
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :note, s_('Admin|Note')
.col-sm-10
= f.text_area :note, class: 'form-control gl-form-input gl-form-textarea'

View File

@ -3,47 +3,39 @@
= form_errors(@user)
%fieldset
%legend= _('Account')
%legend.gl-border-bottom-0= _('Account')
.form-group.row
.col-sm-2.col-form-label
= f.label :name
.col-sm-10
.col-12
= f.label "#{:name} (required)"
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
%span.help-inline * #{_('required')}
.form-group.row
.col-sm-2.col-form-label
= f.label :username
.col-sm-10
.col-12
= f.label "#{:username} (required)"
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control gl-form-input'
%span.help-inline * #{_('required')}
.form-group.row
.col-sm-2.col-form-label
= f.label :email
.col-sm-10
.col-12
= f.label "#{:email} (required)"
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
%span.help-inline * #{_('required')}
- if @user.new_record?
%fieldset
%legend= _('Password')
%legend.gl-border-bottom-0= _('Password')
.form-group.row
.col-sm-2.col-form-label
= f.label :password
.col-sm-10
.col-12
%strong
= _('Reset link will be generated and sent to the user. %{break} User will be forced to set the password on first sign in.').html_safe % { break: '<br />'.html_safe }
- else
%fieldset
%legend= _('Password')
%legend.gl-border-bottom-0= _('Password')
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :password
.col-sm-10
.col-12
= f.password_field :password, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :password_confirmation
.col-sm-10
.col-12
= f.password_field :password_confirmation, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
= render partial: 'access_levels', locals: { f: f }
@ -53,37 +45,33 @@
= render_if_exists 'admin/users/limits', f: f
%fieldset
%legend= _('Profile')
%legend.gl-border-bottom-0= _('Profile')
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :avatar
.col-sm-10
.col-12
= f.file_field :avatar
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :skype
.col-sm-10
= f.text_field :skype, class: 'form-control gl-form-input'
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :linkedin
.col-sm-10
= f.text_field :linkedin, class: 'form-control gl-form-input'
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :twitter
.col-sm-10
= f.text_field :twitter, class: 'form-control gl-form-input'
.form-group.row
.col-sm-2.col-form-label
.col-12
= f.label :website_url
.col-sm-10
= f.text_field :website_url, class: 'form-control gl-form-input'
= render 'admin/users/admin_notes', f: f
.form-actions
%div
- if @user.new_record?
= f.submit _('Create user'), class: "btn gl-button btn-confirm"
= link_to _('Cancel'), admin_users_path, class: "gl-button btn btn-default btn-cancel"

View File

@ -1,5 +1,4 @@
- page_title _("Edit"), @user.name, _("Users")
%h1.page-title
%h1.page-title.gl-font-size-h-display.gl-mb-6
= _("Edit user: %{user_name}") % { user_name: @user.name }
%hr
= render 'form'

View File

@ -1,5 +1,4 @@
- page_title _("New User")
%h1.page-title
%h1.page-title.gl-font-size-h-display.gl-mb-6
= s_('AdminUsers|New user')
%hr
= render 'form'

View File

@ -51,3 +51,6 @@
.settings-content
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
- if ::Feature.enabled?(:group_level_protected_environment, @group)
= render_if_exists 'groups/settings/ci_cd/protected_environments', expanded: expanded

View File

@ -16,7 +16,7 @@
= yield :flash_message
= dispensable_render "shared/service_ping_consent"
= dispensable_render_if_exists "layouts/header/ee_subscribable_banner"
= dispensable_render_if_exists "layouts/header/seats_count_alert"
= dispensable_render_if_exists "layouts/header/seat_count_alert"
= dispensable_render_if_exists "shared/namespace_storage_limit_alert"
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"

View File

@ -1,5 +1,5 @@
%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('chevron-double-lg-left', css_class: 'icon-chevron-double-lg-left')
= sprite_icon('chevron-double-lg-left', size: 12, css_class: 'icon-chevron-double-lg-left')
%span.collapse-text.gl-ml-3= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do

View File

@ -1,3 +1,5 @@
const coreJSVersion = require('./node_modules/core-js/package.json').version;
const BABEL_ENV = process.env.BABEL_ENV || process.env.NODE_ENV || null;
let presets = [
@ -5,7 +7,7 @@ let presets = [
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3, proposals: true },
corejs: { version: coreJSVersion, proposals: true },
modules: false,
},
],

View File

@ -0,0 +1,8 @@
---
name: ci_docker_image_pull_policy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85588
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363186
milestone: '15.1'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -1,8 +1,8 @@
---
name: env_stopped_on_stop_success
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86478
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/361473
milestone: '15.0'
name: seat_count_alerts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89204
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362041
milestone: '15.1'
type: development
group: group::release
group: group::purchase
default_enabled: false

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddSecurityPolicyScanExecutionSchedulesToPlanLimits < Gitlab::Database::Migration[2.0]
def up
add_column(:plan_limits, :security_policy_scan_execution_schedules, :integer, default: 0, null: false)
end
def down
remove_column(:plan_limits, :security_policy_scan_execution_schedules)
end
end

View File

@ -0,0 +1 @@
4b979c4ae290efdbc7c4bfe7105f0b30d00e532ac11c579db7417a317fd35db8

View File

@ -18773,7 +18773,8 @@ CREATE TABLE plan_limits (
dotenv_size integer DEFAULT 5120 NOT NULL,
pipeline_triggers integer DEFAULT 25000 NOT NULL,
project_ci_secure_files integer DEFAULT 100 NOT NULL,
repository_size bigint DEFAULT 0 NOT NULL
repository_size bigint DEFAULT 0 NOT NULL,
security_policy_scan_execution_schedules integer DEFAULT 0 NOT NULL
);
CREATE SEQUENCE plan_limits_id_seq

View File

@ -19,7 +19,7 @@ The steps below should be followed in the order they appear. **Make sure the Git
If you installed GitLab using the Omnibus packages (highly recommended):
1. [Install GitLab Enterprise Edition](https://about.gitlab.com/install/) on the nodes that will serve as the **secondary** site. Do not create an account or log in to the new **secondary** site.
1. [Install GitLab Enterprise Edition](https://about.gitlab.com/install/) on the nodes that will serve as the **secondary** site. Do not create an account or log in to the new **secondary** site. The **GitLab version must match** across primary and secondary sites.
1. [Add the GitLab License](../../../user/admin_area/license.md) on the **primary** site to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher.
1. [Set up the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** sites.

View File

@ -554,6 +554,26 @@ Plan.default.actual_limits.update!(ci_daily_pipeline_schedule_triggers: 1440)
This limit is [enabled on GitLab.com](../user/gitlab_com/index.md#gitlab-cicd).
### Limit the number of schedule rules defined for security policy project
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335659) in GitLab 15.1.
You can limit the total number of schedule rules per security policy project. This limit is
checked each time policies with schedule rules are updated. If a new schedule rule would
cause the total number of schedule rules to exceed the limit, the new schedule rule is
not processed.
By default, self-managed instances do not limit the number of processable schedule rules.
To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
Plan.default.actual_limits.update!(security_policy_scan_execution_schedules: 100)
```
This limit is [enabled on GitLab.com](../user/gitlab_com/index.md#gitlab-cicd).
### Number of instance level variables
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216097) in GitLab 13.1.

View File

@ -1883,6 +1883,52 @@ image:
- [Override the entrypoint of an image](../docker/using_docker_images.md#override-the-entrypoint-of-an-image).
#### `image:pull_policy`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21619) in GitLab 15.1 [with a flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. Disabled by default.
> - Requires GitLab Runner 15.1 or later.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`.
The feature is not ready for production use.
The pull policy that the runner uses to fetch the Docker image.
**Keyword type**: Job keyword. You can use it only as part of a job or in the [`default` section](#default).
**Possible inputs**:
- A single pull policy, or multiple pull policies in an array.
Can be `always`, `if-not-present`, or `never`.
**Examples of `image:pull_policy`**:
```yaml
job1:
script: echo "A single pull policy."
image:
name: ruby:3.0
pull_policy: if-not-present
job2:
script: echo "Multiple pull policies."
image:
name: ruby:3.0
pull_policy: [always, if-not-present]
```
**Additional details**:
- If the runner does not support the defined pull policy, the job fails with an error similar to:
`ERROR: Job failed (system failure): the configured PullPolicies ([always]) are not allowed by AllowedPullPolicies ([never])`.
**Related topics**:
- [Run your CI/CD jobs in Docker containers](../docker/using_docker_images.md).
- [How runner pull policies work](https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work).
- [Using multiple pull policies](https://docs.gitlab.com/runner/executors/docker.html#using-multiple-pull-policies).
### `inherit`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207484) in GitLab 12.9.

View File

@ -23,7 +23,7 @@ The following outline re-uses the [maturity metric](https://about.gitlab.com/dir
- [Release management](#release-management)
- [Enabled on GitLab.com](feature_flags/controls.md#enabling-a-feature-for-gitlabcom)
- Complete
- [Configurable by the GitLab orchestrator](https://gitlab.com/gitlab-org/gitlab-orchestrator)
- [Configurable by the GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit)
- Lovable
- Enabled by default for the majority of users

View File

@ -185,6 +185,7 @@ See the [test engineering process](https://about.gitlab.com/handbook/engineering
##### Observability instrumentation
1. I have included enough instrumentation to facilitate debugging and proactive performance improvements through observability.
See [example](https://gitlab.com/gitlab-org/gitlab/-/issues/346124#expectations) of adding feature flags, logging, and instrumentation.
##### Documentation

View File

@ -185,7 +185,7 @@ gitlab_rails['omniauth_providers'] = [
name: 'saml_1',
args: {
name: 'saml_1', # This is mandatory and must match the provider name
strategy_class: 'OmniAuth::Strategies::SAML'
strategy_class: 'OmniAuth::Strategies::SAML',
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
... # Put here all the required arguments similar to a single provider
},
@ -195,7 +195,7 @@ gitlab_rails['omniauth_providers'] = [
name: 'saml_2',
args: {
name: 'saml_2', # This is mandatory and must match the provider name
strategy_class: 'OmniAuth::Strategies::SAML'
strategy_class: 'OmniAuth::Strategies::SAML',
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
... # Put here all the required arguments similar to a single provider
},

View File

@ -156,6 +156,7 @@ the related documentation.
| Maximum number of pipeline triggers in a project | `25000` for Free tier, Unlimited for all paid tiers | See [Limit the number of pipeline triggers](../../administration/instance_limits.md#limit-the-number-of-pipeline-triggers) |
| Maximum pipeline schedules in projects | `10` for Free tier, `50` for all paid tiers | See [Number of pipeline schedules](../../administration/instance_limits.md#number-of-pipeline-schedules) |
| Maximum pipelines per schedule | `24` for Free tier, `288` for all paid tiers | See [Limit the number of pipelines created by a pipeline schedule per day](../../administration/instance_limits.md#limit-the-number-of-pipelines-created-by-a-pipeline-schedule-per-day) |
| Maximum number of schedule rules defined for each security policy project | Unlimited for all paid tiers | See [Number of schedule rules defined for each security policy project](../../administration/instance_limits.md#limit-the-number-of-schedule-rules-defined-for-security-policy-project) |
| Scheduled job archiving | 3 months (from June 22, 2020). Jobs created before that date were archived after September 22, 2020. | Never |
| Maximum test cases per [unit test report](../../ci/testing/unit_test_reports.md) | `500000` | Unlimited |
| Maximum registered runners | Free tier: `50` per-group / `50` per-project<br/>All paid tiers: `1000` per-group / `1000` per-project | See [Number of registered runners per scope](../../administration/instance_limits.md#number-of-registered-runners-per-scope) |

View File

@ -7,6 +7,8 @@ module API
class Image < Grape::Entity
expose :name, :entrypoint
expose :ports, using: Entities::Ci::JobRequest::Port
expose :pull_policy, if: ->(_) { ::Feature.enabled?(:ci_docker_image_pull_policy) }
end
end
end

View File

@ -4,7 +4,10 @@ module API
module Entities
module Ci
module JobRequest
class Service < Entities::Ci::JobRequest::Image
class Service < Grape::Entity
expose :name, :entrypoint
expose :ports, using: Entities::Ci::JobRequest::Port
expose :alias, :command
expose :variables
end

View File

@ -4,7 +4,7 @@ module Gitlab
module Ci
module Build
class Image
attr_reader :alias, :command, :entrypoint, :name, :ports, :variables
attr_reader :alias, :command, :entrypoint, :name, :ports, :variables, :pull_policy
class << self
def from_image(job)
@ -34,6 +34,7 @@ module Gitlab
@name = image[:name]
@ports = build_ports(image).select(&:valid?)
@variables = build_variables(image)
@pull_policy = image[:pull_policy]
end
end

View File

@ -12,11 +12,13 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[name entrypoint ports].freeze
ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
validations do
validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, allowed_keys: ALLOWED_KEYS, if: :ci_docker_image_pull_policy_enabled?
validates :config, allowed_keys: LEGACY_ALLOWED_KEYS, unless: :ci_docker_image_pull_policy_enabled?
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true
@ -26,7 +28,10 @@ module Gitlab
entry :ports, Entry::Ports,
description: 'Ports used to expose the image'
attributes :ports
entry :pull_policy, Entry::PullPolicy,
description: 'Pull policy for the image'
attributes :ports, :pull_policy
def name
value[:name]
@ -37,16 +42,28 @@ module Gitlab
end
def value
return { name: @config } if string?
return @config if hash?
{}
if string?
{ name: @config }
elsif hash?
{
name: @config[:name],
entrypoint: @config[:entrypoint],
ports: ports_value,
pull_policy: (ci_docker_image_pull_policy_enabled? ? pull_policy_value : nil)
}.compact
else
{}
end
end
def with_image_ports?
opt(:with_image_ports)
end
def ci_docker_image_pull_policy_enabled?
::Feature.enabled?(:ci_docker_image_pull_policy)
end
def skip_config_hash_validation?
true
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a configuration of the pull policies of an image.
#
class PullPolicy < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
ALLOWED_POLICIES = %w[always never if-not-present].freeze
validations do
validates :config, array_of_strings_or_string: true
validates :config,
allowed_array_values: { in: ALLOWED_POLICIES },
presence: true,
if: :array?
validates :config,
inclusion: { in: ALLOWED_POLICIES },
if: :string?
end
def value
# We either return an array with policies or nothing
Array(@config).presence
end
end
end
end
end
end

View File

@ -106,6 +106,10 @@ module Gitlab
@config.is_a?(Hash)
end
def array?
@config.is_a?(Array)
end
def string?
@config.is_a?(String)
end

View File

@ -56,7 +56,11 @@ module Gitlab
end
def copy_file_for_lfs_object(lfs_object)
copy_files(lfs_object.file.path, destination_path_for_object(lfs_object))
file_path = lfs_object.file.path
return unless File.exist?(file_path)
copy_files(file_path, destination_path_for_object(lfs_object))
end
def append_lfs_json_for_batch(lfs_objects_batch)

View File

@ -4,6 +4,23 @@ databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml
namespace :gitlab do
namespace :db do
DB_CONFIG_NAME_KEY = 'gitlab_db_config_name'
DB_IDENTIFIER_SQL = <<-SQL
SELECT system_identifier, current_database()
FROM pg_control_system()
SQL
# We fetch timestamp as a way to properly handle race conditions
# fail in such cases, which should not really happen in production environment
DB_IDENTIFIER_WITH_DB_CONFIG_NAME_SQL = <<-SQL
SELECT
system_identifier, current_database(),
value as db_config_name, created_at as timestamp
FROM pg_control_system()
LEFT JOIN ar_internal_metadata ON ar_internal_metadata.key=$1
SQL
desc 'Validates `config/database.yml` to ensure a correct behavior is configured'
task validate_config: :environment do
original_db_config = ActiveRecord::Base.connection_db_config # rubocop:disable Database/MultipleDatabases
@ -14,26 +31,22 @@ namespace :gitlab do
db_configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, include_replicas: true)
db_configs = db_configs.reject(&:replica?)
# Map each database connection into unique identifier of system+database
# rubocop:disable Database/MultipleDatabases
all_connections = db_configs.map do |db_config|
identifier =
begin
ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
ActiveRecord::Base.connection.select_one("SELECT system_identifier, current_database() FROM pg_control_system()")
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
rescue ActiveRecord::NoDatabaseError
end
# The `pg_control_system()` is not enough to properly discover matching database systems
# since in case of cluster promotion it will return the same identifier as main cluster
# We instead set an `ar_internal_metadata` information with configured database name
db_configs.reverse_each do |db_config|
insert_db_identifier(db_config)
end
# Map each database connection into unique identifier of system+database
all_connections = db_configs.map do |db_config|
{
name: db_config.name,
config: db_config,
database_tasks?: db_config.database_tasks?,
identifier: identifier
identifier: get_db_identifier(db_config)
}
end.compact
# rubocop:enable Database/MultipleDatabases
end
unique_connections = all_connections.group_by { |connection| connection[:identifier] }
primary_connection = all_connections.find { |connection| ActiveRecord::Base.configurations.primary?(connection[:name]) }
@ -111,5 +124,39 @@ namespace :gitlab do
Rake::Task["db:schema:load:#{name}"].enhance(['gitlab:db:validate_config'])
Rake::Task["db:schema:dump:#{name}"].enhance(['gitlab:db:validate_config'])
end
def insert_db_identifier(db_config)
ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
if ActiveRecord::InternalMetadata.table_exists?
ts = Time.zone.now
ActiveRecord::InternalMetadata.upsert(
{ key: DB_CONFIG_NAME_KEY,
value: db_config.name,
created_at: ts,
updated_at: ts }
)
end
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
rescue ActiveRecord::NoDatabaseError
end
def get_db_identifier(db_config)
ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
# rubocop:disable Database/MultipleDatabases
if ActiveRecord::InternalMetadata.table_exists?
ActiveRecord::Base.connection.select_one(
DB_IDENTIFIER_WITH_DB_CONFIG_NAME_SQL, nil, [DB_CONFIG_NAME_KEY])
else
ActiveRecord::Base.connection.select_one(DB_IDENTIFIER_SQL)
end
# rubocop:enable Database/MultipleDatabases
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
rescue ActiveRecord::NoDatabaseError
end
end
end

View File

@ -14996,6 +14996,9 @@ msgstr ""
msgid "Estimated"
msgstr ""
msgid "Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
msgstr ""
msgid "EventFilterBy|Filter by all"
msgstr ""
@ -15543,6 +15546,9 @@ msgstr ""
msgid "Failed to load groups, users and deploy keys."
msgstr ""
msgid "Failed to load groups."
msgstr ""
msgid "Failed to load iteration cadences."
msgstr ""
@ -20995,6 +21001,9 @@ msgstr ""
msgid "InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier."
msgstr ""
msgid "InviteMembersModal|To make more space, you can remove members who no longer need access."
msgstr ""
msgid "InviteMembersModal|Username or email address"
msgstr ""
@ -21004,6 +21013,9 @@ msgstr ""
msgid "InviteMembersModal|You only have space for %{count} more %{members} in %{name}"
msgstr ""
msgid "InviteMembersModal|You only have space for %{count} more %{members} in your personal projects"
msgstr ""
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
msgstr ""
@ -21019,6 +21031,9 @@ msgstr ""
msgid "InviteMembersModal|You've reached your %{count} %{members} limit for %{name}"
msgstr ""
msgid "InviteMembersModal|You've reached your %{count} %{members} limit for your personal projects"
msgstr ""
msgid "InviteMembers|Invite a group"
msgstr ""
@ -30785,6 +30800,9 @@ msgstr ""
msgid "ProtectedEnvironment|Environment"
msgstr ""
msgid "ProtectedEnvironment|Only specified groups can execute deployments in protected environments."
msgstr ""
msgid "ProtectedEnvironment|Only specified users can execute deployments in a protected environment."
msgstr ""
@ -30803,6 +30821,9 @@ msgstr ""
msgid "ProtectedEnvironment|Select an environment"
msgstr ""
msgid "ProtectedEnvironment|Select groups"
msgstr ""
msgid "ProtectedEnvironment|Select users"
msgstr ""
@ -33789,6 +33810,9 @@ msgstr ""
msgid "SecurityOrchestration|%{scanners} %{severities} in an open merge request targeting %{branches}."
msgstr ""
msgid "SecurityOrchestration|+%{count} more"
msgstr ""
msgid "SecurityOrchestration|.yaml mode"
msgstr ""
@ -34122,9 +34146,6 @@ msgstr ""
msgid "SecurityOrchestration|vulnerability"
msgstr ""
msgid "SecurityPolicies|+%{count} more"
msgstr ""
msgid "SecurityReports|%{count} Selected"
msgstr ""
@ -44169,8 +44190,10 @@ msgstr ""
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has %{remaining_seats_count} out of %{total_seats_count} seats remaining. Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
msgstr ""
msgid "Your subscription has %{remaining_seat_count} out of %{total_seat_count} seat remaining."
msgid_plural "Your subscription has %{remaining_seat_count} out of %{total_seat_count} seats remaining."
msgstr[0] ""
msgstr[1] ""
msgid "Your subscription is now expired. To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can add this license to your instance. To use Free tier, remove your current license."
msgstr ""
@ -45885,9 +45908,6 @@ msgstr ""
msgid "repository:"
msgstr ""
msgid "required"
msgstr ""
msgid "satisfied"
msgstr ""

View File

@ -1,4 +1,7 @@
FROM registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-99-docker-20.10.14-gcloud-383-kubectl-1.23
ARG DOCKER_VERSION=20.10.14
ARG CHROME_VERSION=101
FROM registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23
LABEL maintainer="GitLab Quality Department <quality@gitlab.com>"
ENV DEBIAN_FRONTEND="noninteractive"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::WorkItemsFinder do
using RSpec::Parameterized::TableSyntax
include_context 'WorkItemsFinder context'
it_behaves_like 'issues or work items finder', :work_item, 'WorkItemsFinder#execute context'
end

View File

@ -200,6 +200,30 @@ describe('InviteModalBase', () => {
});
});
describe('when user limit is close on a personal namespace', () => {
beforeEach(() => {
createComponent(
{
closeToLimit: true,
reachedLimit: false,
usersLimitDataset: { membersPath, userNamespace: true },
},
{ GlModal, GlFormGroup },
);
});
it('renders correct buttons', () => {
const cancelButton = findCancelButton();
const actionButton = findActionButton();
expect(cancelButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED);
expect(cancelButton.attributes('href')).toBe(membersPath);
expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT);
expect(actionButton.attributes('href')).toBe(); // default submit button
});
});
describe('when users limit is not reached', () => {
const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/;

View File

@ -14,9 +14,15 @@ describe('UserLimitNotification', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const createComponent = (reachedLimit = false, usersLimitDataset = {}) => {
const createComponent = (
closeToLimit = false,
reachedLimit = false,
usersLimitDataset = {},
props = {},
) => {
wrapper = shallowMountExtended(UserLimitNotification, {
propsData: {
closeToLimit,
reachedLimit,
usersLimitDataset: {
freeUsersLimit,
@ -25,6 +31,7 @@ describe('UserLimitNotification', () => {
purchasePath: 'purchasePath',
...usersLimitDataset,
},
...props,
},
provide: { name: 'my group' },
stubs: { GlSprintf },
@ -43,9 +50,26 @@ describe('UserLimitNotification', () => {
});
});
describe('when close to limit with a personal namepace', () => {
beforeEach(() => {
createComponent(true, false, { membersCount: 3, userNamespace: true });
});
it('renders the limit for a personal namespace', () => {
const alert = findAlert();
expect(alert.attributes('title')).toEqual(
'You only have space for 2 more members in your personal projects',
);
expect(alert.text()).toEqual(
'To make more space, you can remove members who no longer need access.',
);
});
});
describe('when close to limit', () => {
it("renders user's limit notification", () => {
createComponent(false, { membersCount: 3 });
createComponent(true, false, { membersCount: 3 });
const alert = findAlert();
@ -61,7 +85,7 @@ describe('UserLimitNotification', () => {
describe('when limit is reached', () => {
it("renders user's limit notification", () => {
createComponent(true);
createComponent(true, true);
const alert = findAlert();
@ -71,12 +95,12 @@ describe('UserLimitNotification', () => {
describe('when free user namespace', () => {
it("renders user's limit notification", () => {
createComponent(true, { userNamespace: true });
createComponent(true, true, { userNamespace: true });
const alert = findAlert();
expect(alert.attributes('title')).toEqual(
"You've reached your 5 members limit for my group",
"You've reached your 5 members limit for your personal projects",
);
expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE);

View File

@ -373,16 +373,16 @@ describe('Linked pipeline', () => {
describe('expand button', () => {
it.each`
pipelineType | anglePosition | buttonBorderClasses | expanded
${downstreamProps} | ${'angle-right'} | ${'gl-border-l-0!'} | ${false}
${downstreamProps} | ${'angle-left'} | ${'gl-border-l-0!'} | ${true}
${upstreamProps} | ${'angle-left'} | ${'gl-border-r-0!'} | ${false}
${upstreamProps} | ${'angle-right'} | ${'gl-border-r-0!'} | ${true}
pipelineType | chevronPosition | buttonBorderClasses | expanded
${downstreamProps} | ${'chevron-lg-right'} | ${'gl-border-l-0!'} | ${false}
${downstreamProps} | ${'chevron-lg-left'} | ${'gl-border-l-0!'} | ${true}
${upstreamProps} | ${'chevron-lg-left'} | ${'gl-border-r-0!'} | ${false}
${upstreamProps} | ${'chevron-lg-right'} | ${'gl-border-r-0!'} | ${true}
`(
'$pipelineType.columnTitle pipeline button icon should be $anglePosition with $buttonBorderClasses if expanded state is $expanded',
({ pipelineType, anglePosition, buttonBorderClasses, expanded }) => {
'$pipelineType.columnTitle pipeline button icon should be $chevronPosition with $buttonBorderClasses if expanded state is $expanded',
({ pipelineType, chevronPosition, buttonBorderClasses, expanded }) => {
createWrapper({ propsData: { ...pipelineType, expanded } });
expect(findExpandButton().props('icon')).toBe(anglePosition);
expect(findExpandButton().props('icon')).toBe(chevronPosition);
expect(findExpandButton().classes()).toContain(buttonBorderClasses);
},
);

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::Entities::Ci::JobRequest::Image do
let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]}
let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)}
let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports, pull_policy: ['if-not-present']) }
let(:entity) { described_class.new(image) }
subject { entity.as_json }
@ -28,4 +28,18 @@ RSpec.describe API::Entities::Ci::JobRequest::Image do
expect(subject[:ports]).to be_nil
end
end
it 'returns the pull policy' do
expect(subject[:pull_policy]).to eq(['if-not-present'])
end
context 'when the FF ci_docker_image_pull_policy is disabled' do
before do
stub_feature_flags(ci_docker_image_pull_policy: false)
end
it 'does not return the pull policy' do
expect(subject).not_to have_key(:pull_policy)
end
end
end

View File

@ -28,8 +28,14 @@ RSpec.describe Gitlab::Ci::Build::Image do
context 'when image is defined as hash' do
let(:entrypoint) { '/bin/sh' }
let(:pull_policy) { %w[always if-not-present] }
let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) }
let(:job) do
create(:ci_build, options: { image: { name: image_name,
entrypoint: entrypoint,
ports: [80],
pull_policy: pull_policy } } )
end
it 'fabricates an object of the proper class' do
is_expected.to be_kind_of(described_class)
@ -38,6 +44,7 @@ RSpec.describe Gitlab::Ci::Build::Image do
it 'populates fabricated object with the proper attributes' do
expect(subject.name).to eq(image_name)
expect(subject.entrypoint).to eq(entrypoint)
expect(subject.pull_policy).to eq(pull_policy)
end
it 'populates the ports' do

View File

@ -1,8 +1,16 @@
# frozen_string_literal: true
require 'spec_helper'
require 'fast_spec_helper'
require 'support/helpers/stubbed_feature'
require 'support/helpers/stub_feature_flags'
RSpec.describe Gitlab::Ci::Config::Entry::Image do
include StubFeatureFlags
before do
stub_feature_flags(ci_docker_image_pull_policy: true)
end
let(:entry) { described_class.new(config) }
context 'when configuration is a string' do
@ -43,6 +51,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
expect(entry.ports).to be_nil
end
end
describe '#pull_policy' do
it "returns nil" do
expect(entry.pull_policy).to be_nil
end
end
end
context 'when configuration is a hash' do
@ -109,6 +123,56 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
end
end
end
context 'when configuration has pull_policy' do
let(:config) { { name: 'image:1.0', pull_policy: 'if-not-present' } }
describe '#valid?' do
it 'is valid' do
entry.compose!
expect(entry).to be_valid
end
context 'when the feature flag ci_docker_image_pull_policy is disabled' do
before do
stub_feature_flags(ci_docker_image_pull_policy: false)
end
it 'is not valid' do
entry.compose!
expect(entry).not_to be_valid
expect(entry.errors).to include('image config contains unknown keys: pull_policy')
end
end
end
describe '#value' do
it "returns value" do
entry.compose!
expect(entry.value).to eq(
name: 'image:1.0',
pull_policy: ['if-not-present']
)
end
context 'when the feature flag ci_docker_image_pull_policy is disabled' do
before do
stub_feature_flags(ci_docker_image_pull_policy: false)
end
it 'is not valid' do
entry.compose!
expect(entry.value).to eq(
name: 'image:1.0'
)
end
end
end
end
end
context 'when entry value is not correct' do

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::PullPolicy do
let(:entry) { described_class.new(config) }
describe '#value' do
subject(:value) { entry.value }
context 'when config value is nil' do
let(:config) { nil }
it { is_expected.to be_nil }
end
context 'when retry value is an empty array' do
let(:config) { [] }
it { is_expected.to eq(nil) }
end
context 'when retry value is string' do
let(:config) { "always" }
it { is_expected.to eq(%w[always]) }
end
context 'when retry value is array' do
let(:config) { %w[always if-not-present] }
it { is_expected.to eq(%w[always if-not-present]) }
end
end
describe 'validation' do
subject(:valid?) { entry.valid? }
context 'when retry value is nil' do
let(:config) { nil }
it { is_expected.to eq(false) }
end
context 'when retry value is an empty array' do
let(:config) { [] }
it { is_expected.to eq(false) }
end
context 'when retry value is a hash' do
let(:config) { {} }
it { is_expected.to eq(false) }
end
context 'when retry value is string' do
let(:config) { "always" }
it { is_expected.to eq(true) }
context 'when it is an invalid policy' do
let(:config) { "invalid" }
it { is_expected.to eq(false) }
end
context 'when it is an empty string' do
let(:config) { "" }
it { is_expected.to eq(false) }
end
end
context 'when retry value is array' do
let(:config) { %w[always if-not-present] }
it { is_expected.to eq(true) }
context 'when config contains an invalid policy' do
let(:config) { %w[always invalid] }
it { is_expected.to eq(false) }
end
end
end
end

View File

@ -7,7 +7,7 @@ module Gitlab
RSpec.describe YamlProcessor do
include StubRequests
subject { described_class.new(config, user: nil).execute }
subject(:processor) { described_class.new(config, user: nil).execute }
shared_examples 'returns errors' do |error_message|
it 'adds a message when an error is encountered' do
@ -965,6 +965,51 @@ module Gitlab
})
end
end
context 'when image has pull_policy' do
let(:config) do
<<~YAML
image:
name: ruby:2.7
pull_policy: if-not-present
test:
script: exit 0
YAML
end
it { is_expected.to be_valid }
it "returns image and service when defined" do
expect(processor.stage_builds_attributes("test")).to contain_exactly({
stage: "test",
stage_idx: 2,
name: "test",
only: { refs: %w[branches tags] },
options: {
script: ["exit 0"],
image: { name: "ruby:2.7", pull_policy: ["if-not-present"] }
},
allow_failure: false,
when: "on_success",
job_variables: [],
root_variables_inheritance: true,
scheduling_type: :stage
})
end
context 'when the feature flag ci_docker_image_pull_policy is disabled' do
before do
stub_feature_flags(ci_docker_image_pull_policy: false)
end
it { is_expected.not_to be_valid }
it "returns no job" do
expect(processor.jobs).to eq({})
end
end
end
end
describe 'Variables' do

View File

@ -45,6 +45,18 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
end
context 'when lfs object has file on disk missing' do
it 'does not attempt to copy non-existent file' do
FileUtils.rm(lfs_object.file.path)
expect(saver).not_to receive(:copy_files)
saver.save # rubocop:disable Rails/SaveBang
expect(shared.errors).to be_empty
expect(File).not_to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
end
end
describe 'saving a json file' do
before do
# Create two more LfsObjectProject records with different `repository_type`s

View File

@ -31,7 +31,7 @@ RSpec.describe Limitable do
it 'triggers scoped validations' do
instance = MinimalTestClass.new
expect(instance).to receive(:validate_scoped_plan_limit_not_exceeded)
expect(instance).to receive(:scoped_plan_limits)
instance.valid?(:create)
end
@ -94,7 +94,7 @@ RSpec.describe Limitable do
it 'triggers scoped validations' do
instance = MinimalTestClass.new
expect(instance).to receive(:validate_global_plan_limit_not_exceeded)
expect(instance).to receive(:global_plan_limits)
instance.valid?(:create)
end

View File

@ -587,22 +587,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(action.user).to eq(user)
end
context 'env_stopped_on_stop_success feature flag' do
it 'environment is not stopped when flag is enabled' do
stub_feature_flags(env_stopped_on_stop_success: true)
it 'environment is not stopped' do
subject
subject
expect(environment).not_to be_stopped
end
it 'environment is stopped when flag is disabled' do
stub_feature_flags(env_stopped_on_stop_success: false)
subject
expect(environment).to be_stopped
end
expect(environment).not_to be_stopped
end
end

View File

@ -215,6 +215,7 @@ RSpec.describe PlanLimits do
web_hook_calls
ci_daily_pipeline_schedule_triggers
repository_size
security_policy_scan_execution_schedules
] + disabled_max_artifact_size_columns
end

View File

@ -32,7 +32,7 @@ RSpec.describe ProjectPolicy do
end
end
it 'does not include the read_issue permission when the issue author is not a member of the private project' do
it 'does not include the read permissions when the issue author is not a member of the private project' do
project = create(:project, :private)
issue = create(:issue, project: project, author: create(:user))
user = issue.author
@ -40,6 +40,7 @@ RSpec.describe ProjectPolicy do
expect(project.team.member?(issue.author)).to be false
expect(Ability).not_to be_allowed(user, :read_issue, project)
expect(Ability).not_to be_allowed(user, :read_work_item, project)
end
it_behaves_like 'model with wiki policies' do
@ -61,7 +62,7 @@ RSpec.describe ProjectPolicy do
end
it 'does not include the issues permissions' do
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task, :read_work_item
end
it 'disables boards and lists permissions' do
@ -73,7 +74,7 @@ RSpec.describe ProjectPolicy do
it 'does not include the issues permissions' do
create(:jira_integration, project: project)
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task, :read_work_item
end
end
end
@ -752,14 +753,14 @@ RSpec.describe ProjectPolicy do
allow(project).to receive(:service_desk_enabled?).and_return(true)
end
it { expect_allowed(:reporter_access, :create_note, :read_issue) }
it { expect_allowed(:reporter_access, :create_note, :read_issue, :read_work_item) }
context 'when issues are protected members only' do
before do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
end
it { expect_allowed(:reporter_access, :create_note, :read_issue) }
it { expect_allowed(:reporter_access, :create_note, :read_issue, :read_work_item) }
end
end
end

View File

@ -37,6 +37,12 @@ RSpec.describe WorkItemPolicy do
let(:current_user) { guest_author }
it { is_expected.to be_allowed(:read_work_item) }
context 'when work_item is confidential' do
let(:work_item_subject) { create(:work_item, confidential: true, project: project) }
it { is_expected.not_to be_allowed(:read_work_item) }
end
end
end

View File

@ -216,7 +216,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_info)
expect(json_response['git_info']).to eq(expected_git_info)
expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [] })
expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => nil },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
@ -810,6 +810,45 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
context 'when image has pull_policy' do
let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
let(:options) do
{
image: {
name: 'ruby',
pull_policy: ['if-not-present']
}
}
end
it 'returns the image with pull policy' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include(
'id' => job.id,
'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] }
)
end
context 'when the FF ci_docker_image_pull_policy is disabled' do
before do
stub_feature_flags(ci_docker_image_pull_policy: false)
end
it 'returns the image without pull policy' do
request_job
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to include(
'id' => job.id,
'image' => { 'name' => 'ruby', 'entrypoint' => nil, 'ports' => [] }
)
end
end
end
describe 'a job with excluded artifacts' do
context 'when excluded paths are defined' do
let(:job) do

View File

@ -53,6 +53,18 @@ RSpec.describe BulkImports::LfsObjectsExportService do
)
end
context 'when lfs object has file on disk missing' do
it 'does not attempt to copy non-existent file' do
FileUtils.rm(lfs_object.file.path)
expect(service).not_to receive(:copy_files)
service.execute
expect(File).not_to exist(File.join(export_path, lfs_object.oid))
end
end
context 'when lfs object is remotely stored' do
let(:lfs_object) { create(:lfs_object, :object_storage) }

View File

@ -12,7 +12,7 @@ RSpec.shared_context 'IssuesFinder context' do
let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) }
let_it_be(:label) { create(:label, project: project2) }
let_it_be(:label2) { create(:label, project: project2) }
let_it_be(:issue1, reload: true) do
let_it_be(:item1, reload: true) do
create(:issue,
author: user,
assignees: [user],
@ -23,7 +23,7 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 1.week.ago)
end
let_it_be(:issue2, reload: true) do
let_it_be(:item2, reload: true) do
create(:issue,
author: user,
assignees: [user],
@ -33,7 +33,7 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 1.week.from_now)
end
let_it_be(:issue3, reload: true) do
let_it_be(:item3, reload: true) do
create(:issue,
author: user2,
assignees: [user2],
@ -44,8 +44,8 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 2.weeks.from_now)
end
let_it_be(:issue4, reload: true) { create(:issue, project: project3) }
let_it_be(:issue5, reload: true) do
let_it_be(:item4, reload: true) { create(:issue, project: project3) }
let_it_be(:item5, reload: true) do
create(:issue,
author: user,
assignees: [user],
@ -55,18 +55,20 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 3.days.ago)
end
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
let(:items_model) { Issue }
end
RSpec.shared_context 'IssuesFinder#execute context' do
let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
let!(:label_link) { create(:label_link, label: label, target: issue2) }
let!(:label_link2) { create(:label_link, label: label2, target: issue3) }
let!(:closed_item) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
let!(:label_link) { create(:label_link, label: label, target: item2) }
let!(:label_link2) { create(:label_link, label: label2, target: item3) }
let(:search_user) { user }
let(:params) { {} }
let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
before_all do
project1.add_maintainer(user)

View File

@ -0,0 +1,79 @@
# frozen_string_literal: true
RSpec.shared_context 'WorkItemsFinder context' do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project1, reload: true) { create(:project, group: group) }
let_it_be(:project2, reload: true) { create(:project) }
let_it_be(:project3, reload: true) { create(:project, group: subgroup) }
let_it_be(:release) { create(:release, project: project1, tag: 'v1.0.0') }
let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) }
let_it_be(:label) { create(:label, project: project2) }
let_it_be(:label2) { create(:label, project: project2) }
let_it_be(:item1, reload: true) do
create(:work_item,
author: user,
assignees: [user],
project: project1,
milestone: milestone,
title: 'gitlab',
created_at: 1.week.ago,
updated_at: 1.week.ago)
end
let_it_be(:item2, reload: true) do
create(:work_item,
author: user,
assignees: [user],
project: project2,
description: 'gitlab',
created_at: 1.week.from_now,
updated_at: 1.week.from_now)
end
let_it_be(:item3, reload: true) do
create(:work_item,
author: user2,
assignees: [user2],
project: project2,
title: 'tanuki',
description: 'tanuki',
created_at: 2.weeks.from_now,
updated_at: 2.weeks.from_now)
end
let_it_be(:item4, reload: true) { create(:work_item, project: project3) }
let_it_be(:item5, reload: true) do
create(:work_item,
author: user,
assignees: [user],
project: project1,
title: 'wotnot',
created_at: 3.days.ago,
updated_at: 3.days.ago)
end
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
let(:items_model) { WorkItem }
end
RSpec.shared_context 'WorkItemsFinder#execute context' do
let!(:closed_item) { create(:work_item, author: user2, assignees: [user2], project: project2, state: 'closed') }
let!(:label_link) { create(:label_link, label: label, target: item2) }
let!(:label_link2) { create(:label_link, label: label2, target: item3) }
let(:search_user) { user }
let(:params) { {} }
let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
before_all do
project1.add_maintainer(user)
project2.add_developer(user)
project2.add_developer(user2)
project3.add_developer(user)
end
end

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,30 @@
# frozen_string_literal: true
RSpec.shared_examples 'includes Limitable concern' do
describe '#exceeds_limits?' do
let(:plan_limits) { create(:plan_limits, :default_plan) }
context 'without plan limits configured' do
it { expect(subject.exceeds_limits?).to eq false }
end
context 'without plan limits configured' do
before do
plan_limits.update!(subject.class.limit_name => 1)
end
it { expect(subject.exceeds_limits?).to eq false }
context 'with an existing model' do
before do
subject.clone.save!
end
it { expect(subject.exceeds_limits?).to eq true }
end
end
end
describe 'validations' do
let(:plan_limits) { create(:plan_limits, :default_plan) }

View File

@ -3,6 +3,10 @@
require 'rake_helper'
RSpec.describe 'gitlab:db:validate_config', :silence_stdout do
# We don't need to delete this data since it only modifies `ar_internal_metadata`
# which would not be cleaned either by `DbCleaner`
self.use_transactional_tests = false
before :all do
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
@ -111,6 +115,26 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout do
end
it_behaves_like 'validates successfully'
context 'when config is pointing to incorrect server' do
let(:test_config) do
{
main: main_database_config.merge(port: 11235)
}
end
it_behaves_like 'validates successfully'
end
context 'when config is pointing to non-existent database' do
let(:test_config) do
{
main: main_database_config.merge(database: 'non_existent_database')
}
end
it_behaves_like 'validates successfully'
end
end
context 'when main: uses database_tasks=false' do

View File

@ -28,22 +28,10 @@ RSpec.describe BuildSuccessWorker do
it 'does not stop the environment' do
expect(environment).to be_available
stub_feature_flags(env_stopped_on_stop_success: true)
subject
expect(environment.reload).not_to be_stopped
end
it 'does stop the environment when feature flag is disabled' do
expect(environment).to be_available
stub_feature_flags(env_stopped_on_stop_success: false)
subject
expect(environment.reload).to be_stopped
end
end
end
end