Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-04 18:10:11 +00:00
parent ca89460cfa
commit db3acec198
72 changed files with 1392 additions and 545 deletions

View File

@ -364,7 +364,6 @@ linters:
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
- 'ee/app/views/shared/issuable/_filter_weight.html.haml'
- 'ee/app/views/shared/members/ee/_ldap_tag.html.haml'
- 'ee/app/views/shared/members/ee/_override_member_buttons.html.haml'
- 'ee/app/views/shared/members/ee/_sso_badge.html.haml'
- 'ee/app/views/shared/milestones/_burndown.html.haml'
- 'ee/app/views/shared/milestones/_weight.html.haml'

View File

@ -189,7 +189,7 @@ group :puma do
end
# State machine
gem 'state_machines-activerecord', '~> 0.6.0'
gem 'state_machines-activerecord', '~> 0.8.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 6.0'

View File

@ -1144,12 +1144,12 @@ GEM
sshkey (2.0.0)
stackprof (0.2.15)
state_machines (0.5.0)
state_machines-activemodel (0.7.1)
activemodel (>= 4.1)
state_machines-activemodel (0.8.0)
activemodel (>= 5.1)
state_machines (>= 0.5.0)
state_machines-activerecord (0.6.0)
activerecord (>= 4.1)
state_machines-activemodel (>= 0.5.0)
state_machines-activerecord (0.8.0)
activerecord (>= 5.1)
state_machines-activemodel (>= 0.8.0)
swd (1.1.2)
activesupport (>= 3)
attr_required (>= 0.0.5)
@ -1513,7 +1513,7 @@ DEPENDENCIES
sprockets (~> 3.7.0)
sshkey (~> 2.0)
stackprof (~> 0.2.15)
state_machines-activerecord (~> 0.6.0)
state_machines-activerecord (~> 0.8.0)
sys-filesystem (~> 1.1.6)
terser (= 1.0.2)
test-prof (~> 0.12.0)

View File

@ -18,16 +18,16 @@ export default {
};
},
computed: {
...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }),
...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
hasDueDate() {
return this.issue.dueDate != null;
return this.activeIssue.dueDate != null;
},
parsedDueDate() {
if (!this.hasDueDate) {
return null;
}
return parsePikadayDate(this.issue.dueDate);
return parsePikadayDate(this.activeIssue.dueDate);
},
formattedDueDate() {
if (!this.hasDueDate) {

View File

@ -58,20 +58,20 @@ export default {
},
},
computed: {
...mapGetters({ issue: 'activeIssue' }),
...mapGetters(['activeIssue']),
hasMilestone() {
return this.issue.milestone !== null;
return this.activeIssue.milestone !== null;
},
groupFullPath() {
const { referencePath = '' } = this.issue;
const { referencePath = '' } = this.activeIssue;
return referencePath.slice(0, referencePath.indexOf('/'));
},
projectPath() {
const { referencePath = '' } = this.issue;
const { referencePath = '' } = this.activeIssue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
dropdownText() {
return this.issue.milestone?.title ?? this.$options.i18n.noMilestone;
return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone;
},
},
mounted() {
@ -120,7 +120,7 @@ export default {
@close="edit = false"
>
<template v-if="hasMilestone" #collapsed>
<strong class="gl-text-gray-900">{{ issue.milestone.title }}</strong>
<strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong>
</template>
<template>
<gl-dropdown
@ -133,7 +133,7 @@ export default {
<gl-dropdown-item
data-testid="no-milestone-item"
:is-check-item="true"
:is-checked="!issue.milestone"
:is-checked="!activeIssue.milestone"
@click="setMilestone(null)"
>
{{ $options.i18n.noMilestone }}
@ -145,7 +145,7 @@ export default {
v-for="milestone in milestones"
:key="milestone.id"
:is-check-item="true"
:is-checked="issue.milestone && milestone.id === issue.milestone.id"
:is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id"
data-testid="milestone-item"
@click="setMilestone(milestone.id)"
>

View File

@ -15,7 +15,7 @@ export default {
),
},
updateSubscribedErrorMessage: s__(
'IssueBoards|An error occurred while setting notifications status.',
'IssueBoards|An error occurred while setting notifications status. Please try again.',
),
},
components: {

View File

@ -1,5 +1,4 @@
import Vue from 'vue';
import Members from 'ee_else_ce/members';
import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
@ -66,5 +65,4 @@ memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new

View File

@ -1,5 +1,5 @@
import Vue from 'vue';
import Members from 'ee_else_ce/members';
import Members from '~/members';
import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';

View File

@ -1,66 +1,51 @@
<script>
/* eslint-disable vue/no-v-html */
// We are forced to use `v-html` untill this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
// then we can re-write this to use gl-breadcrumb
import { initial, first, last } from 'lodash';
import { sanitize } from '~/lib/dompurify';
// We are using gl-breadcrumb only at the last child of the handwritten breadcrumb
// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
//
// See the CSS workaround in app/assets/stylesheets/pages/registry.scss when this file is changed.
import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
export default {
props: {
crumbs: {
type: Array,
required: true,
},
components: {
GlBreadcrumb,
GlIcon,
},
computed: {
parsedCrumbs() {
return this.crumbs.map((c) => ({ ...c, innerHTML: sanitize(c.innerHTML) }));
},
rootRoute() {
return this.$router.options.routes.find((r) => r.meta.root);
},
detailsRoute() {
return this.$router.options.routes.find((r) => r.name === 'details');
},
isRootRoute() {
return this.$route.name === this.rootRoute.name;
},
rootCrumbs() {
return initial(this.parsedCrumbs);
isLoaded() {
return this.isRootRoute || this.$store?.state.imageDetails?.name;
},
divider() {
const { classList, tagName, innerHTML } = first(this.crumbs).querySelector('svg');
return { classList: [...classList], tagName, innerHTML: sanitize(innerHTML) };
},
lastCrumb() {
const { children } = last(this.crumbs);
const { tagName, className } = first(children);
return {
tagName,
className,
text: this.$route.meta.nameGenerator(),
path: { to: this.$route.name },
};
allCrumbs() {
const crumbs = [
{
text: this.rootRoute.meta.nameGenerator(),
to: this.rootRoute.path,
},
];
if (!this.isRootRoute) {
crumbs.push({
text: this.detailsRoute.meta.nameGenerator(),
href: this.detailsRoute.meta.path,
});
}
return crumbs;
},
},
};
</script>
<template>
<ul>
<li
v-for="(crumb, index) in rootCrumbs"
:key="index"
:class="crumb.className"
v-html="crumb.innerHTML"
></li>
<li v-if="!isRootRoute">
<router-link ref="rootRouteLink" :to="rootRoute.path">
{{ rootRoute.meta.nameGenerator() }}
</router-link>
<component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" />
</li>
<li>
<component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.className">
<router-link ref="childRouteLink" :to="lastCrumb.path">{{ lastCrumb.text }}</router-link>
</component>
</li>
</ul>
<gl-breadcrumb :key="isLoaded" :items="allCrumbs">
<template #separator>
<gl-icon name="angle-right" :size="8" />
</template>
</gl-breadcrumb>
</template>

View File

@ -71,16 +71,28 @@ export default () => {
});
const attachBreadcrumb = () => {
const breadCrumbEl = document.querySelector('nav .js-breadcrumbs-list');
const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')];
const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
const crumbs = [breadCrumbEl.querySelector('h2')];
const nestedBreadcrumbEl = document.createElement('div');
breadCrumbEl.replaceChild(nestedBreadcrumbEl, breadCrumbEl.querySelector('h2'));
return new Vue({
el: breadCrumbEl,
el: nestedBreadcrumbEl,
router,
apolloProvider,
components: {
RegistryBreadcrumb,
},
render(createElement) {
// FIXME(@tnir): this is a workaround until the MR gets merged:
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
const parentEl = breadCrumbEl.parentElement.parentElement;
if (parentEl) {
parentEl.classList.remove('breadcrumbs-container');
parentEl.classList.add('gl-display-flex');
parentEl.classList.add('w-100');
}
// End of FIXME(@tnir)
return createElement('registry-breadcrumb', {
class: breadCrumbEl.className,
props: {

View File

@ -25,12 +25,13 @@ export default {
class="mr-commit-dropdown"
>
<gl-dropdown-item
v-for="commit in commits"
:key="commit.short_id"
v-for="(commit, index) in commits"
:key="index"
class="text-nowrap text-truncate"
@click="$emit('input', commit.message)"
>
<span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
<span class="monospace mr-2">{{ commit.shortId || commit.short_id }}</span>
{{ commit.title }}
</gl-dropdown-item>
</gl-dropdown>
</div>

View File

@ -9,13 +9,18 @@ import {
GlSprintf,
GlLink,
GlTooltipDirective,
GlSkeletonLoader,
} from '@gitlab/ui';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
import MergeRequest from '../../../merge_request';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import MergeRequestStore from '../../stores/mr_widget_store';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import SquashBeforeMerge from './squash_before_merge.vue';
@ -35,6 +40,31 @@ const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error';
export default {
name: 'ReadyToMerge',
apollo: {
state: {
query: readyToMergeQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
manual: true,
result({ data }) {
this.state = {
...data.project.mergeRequest,
mergeRequestsFfOnlyEnabled: data.mergeRequestsFfOnlyEnabled,
onlyAllowMergeIfPipelineSucceeds: data.onlyAllowMergeIfPipelineSucceeds,
};
this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch;
this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
this.isSquashReadOnly = data.project.squashReadOnly;
this.squashCommitMessage = data.project.mergeRequest.defaultSquashCommitMessage;
this.loading = false;
},
},
},
components: {
statusIcon,
SquashBeforeMerge,
@ -48,6 +78,7 @@ export default {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlSkeletonLoader,
MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
@ -58,13 +89,15 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [readyToMergeMixin],
mixins: [readyToMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
loading: this.glFeatures.mergeRequestWidgetGraphql,
state: {},
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
isMakingRequest: false,
isMergingImmediately: false,
@ -75,13 +108,93 @@ export default {
};
},
computed: {
stateData() {
return this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr;
},
hasCI() {
return this.stateData.hasCI || this.stateData.hasCi;
},
isAutoMergeAvailable() {
return !isEmpty(this.mr.availableAutoMergeStrategies);
return !isEmpty(this.stateData.availableAutoMergeStrategies);
},
pipeline() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.pipelines?.nodes?.[0];
}
return this.mr.pipeline;
},
isPipelineFailed() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return ['FAILED', 'CANCELED'].indexOf(this.pipeline?.status) !== -1;
}
return this.mr.isPipelineFailed;
},
isMergeAllowed() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.mergeable || false;
}
return this.mr.isMergeAllowed;
},
canRemoveSourceBranch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.userPermissions.removeSourceBranch;
}
return this.mr.canRemoveSourceBranch;
},
commits() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.commitsWithoutMergeCommits.nodes;
}
return this.mr.commits;
},
commitsCount() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.commitCount || 0;
}
return this.mr.commitsCount;
},
preferredAutoMergeStrategy() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return MergeRequestStore.getPreferredAutoMergeStrategy(
this.state.availableAutoMergeStrategies,
);
}
return this.mr.preferredAutoMergeStrategy;
},
isSHAMismatch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.mr.sha !== this.state.diffHeadSha;
}
return this.mr.isSHAMismatch;
},
squashIsSelected() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.squashReadOnly ? this.state.squashOnMerge : this.state.squash;
}
return this.mr.squashIsSelected;
},
isPipelineActive() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.pipeline?.active || false;
}
return this.mr.isPipelineActive;
},
status() {
const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr;
const ciStatus = this.glFeatures.mergeRequestWidgetGraphql
? this.pipeline?.status.toLowerCase()
: this.mr.ciStatus;
if ((hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) {
if ((this.hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) {
return PIPELINE_FAILED_STATE;
}
@ -89,7 +202,7 @@ export default {
return PIPELINE_PENDING_STATE;
}
if (pipeline && isPipelineFailed) {
if (this.pipeline && this.isPipelineFailed) {
return PIPELINE_FAILED_STATE;
}
@ -114,7 +227,7 @@ export default {
if (
this.status === PIPELINE_FAILED_STATE ||
!this.commitMessage.length ||
!this.mr.isMergeAllowed ||
!this.isMergeAllowed ||
this.mr.preventMerge
) {
return WARNING;
@ -133,27 +246,31 @@ export default {
return __('Merge');
},
hasPipelineMustSucceedConflict() {
return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds;
return !this.hasCI && this.stateData.onlyAllowMergeIfPipelineSucceeds;
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
},
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge, squashIsReadonly, squashIsSelected } = this.mr;
const { enableSquashBeforeMerge } = this.mr;
if (squashIsReadonly && !squashIsSelected) {
if (this.isSquashReadOnly && !this.squashIsSelected) {
return false;
}
return enableSquashBeforeMerge && commitsCount > 1;
return enableSquashBeforeMerge && this.commitsCount > 1;
},
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.isAutoMergeAvailable;
return this.isMergeAllowed || this.isAutoMergeAvailable;
},
shouldShowSquashEdit() {
return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
},
shouldShowMergeEdit() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return !this.state.mergeRequestsFfOnlyEnabled;
}
return !this.mr.ffOnlyEnabled;
},
shaMismatchLink() {
@ -162,18 +279,26 @@ export default {
},
methods: {
updateMergeCommitMessage(includeDescription) {
const { commitMessageWithDescription, commitMessage } = this.mr;
const commitMessage = this.glFeatures.mergeRequestWidgetGraphql
? this.state.defaultMergeCommitMessage
: this.mr.commitMessage;
const commitMessageWithDescription = this.glFeatures.mergeRequestWidgetGraphql
? this.state.defaultMergeCommitMessageWithDescription
: this.mr.commitMessageWithDescription;
this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
},
handleMergeButtonClick(useAutoMerge, mergeImmediately = false) {
if (mergeImmediately) {
this.isMergingImmediately = true;
}
const latestSha = this.glFeatures.mergeRequestWidgetGraphql
? this.state.diffHeadSha
: this.mr.latestSHA;
const options = {
sha: this.mr.latestSHA || this.mr.sha,
sha: latestSha || this.mr.sha,
commit_message: this.commitMessage,
auto_merge_strategy: useAutoMerge ? this.mr.preferredAutoMergeStrategy : undefined,
auto_merge_strategy: useAutoMerge ? this.preferredAutoMergeStrategy : undefined,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
};
@ -294,156 +419,168 @@ export default {
<template>
<div>
<div class="mr-widget-body media" :class="{ 'gl-pb-3': shouldRenderMergeTrainHelperText }">
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<gl-button-group>
<gl-button
size="medium"
category="primary"
class="qa-merge-button accept-merge-request"
:variant="mergeButtonVariant"
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
<gl-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
variant="info"
data-qa-selector="merge_moment_dropdown"
toggle-class="btn-icon js-merge-moment"
>
<template #button-content>
<gl-icon name="chevron-down" class="mr-0" />
<span class="sr-only">{{ __('Select merge moment') }}</span>
</template>
<gl-dropdown-item
icon-name="warning"
button-class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
@click="handleMergeImmediatelyButtonClick"
>
{{ __('Merge immediately') }}
</gl-dropdown-item>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
</gl-dropdown>
</gl-button-group>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls">
<label v-if="mr.canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
class="js-remove-source-branch-checkbox"
type="checkbox"
/>
{{ __('Delete source branch') }}
</label>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isSquashReadOnly"
/>
</template>
<template v-else>
<div class="bold js-resolve-mr-widget-items-message">
<div
v-if="hasPipelineMustSucceedConflict"
class="gl-display-flex gl-align-items-center"
data-testid="pipeline-succeed-conflict"
>
<gl-sprintf :message="pipelineMustSucceedConflictText" />
<gl-link
:href="mr.pipelineMustSucceedDocsPath"
target="_blank"
class="gl-display-flex gl-ml-2"
>
<gl-icon name="question" />
</gl-link>
</div>
<gl-sprintf v-else :message="mergeDisabledText" />
</div>
</template>
</div>
</div>
<div v-if="mr.isSHAMismatch" class="d-flex align-items-center mt-2 js-sha-mismatch">
<gl-icon name="warning-solid" class="text-warning mr-1" />
<span class="text-warning">
<gl-sprintf
:message="
__('New changes were added. %{linkStart}Reload the page to review them%{linkEnd}')
"
>
<template #link="{ content }">
<gl-link :href="mr.mergeRequestDiffsPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</div>
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
<gl-skeleton-loader :width="418" :height="30">
<rect x="0" y="3" width="24" height="24" rx="4" />
<rect x="32" y="0" width="70" height="30" rx="4" />
<rect x="110" y="7" width="150" height="16" rx="4" />
<rect x="268" y="7" width="150" height="16" rx="4" />
</gl-skeleton-loader>
</div>
</div>
<merge-train-helper-text
v-if="shouldRenderMergeTrainHelperText"
:pipeline-id="mr.pipeline.id"
:pipeline-link="mr.pipeline.path"
:merge-train-length="mr.mergeTrainsCount"
:merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
/>
<template v-if="shouldShowMergeControls">
<div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
<template v-else>
<div class="mr-widget-body media" :class="{ 'gl-pb-3': shouldRenderMergeTrainHelperText }">
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<gl-button-group>
<gl-button
size="medium"
category="primary"
class="qa-merge-button accept-merge-request"
:variant="mergeButtonVariant"
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
<gl-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
variant="info"
data-qa-selector="merge_moment_dropdown"
toggle-class="btn-icon js-merge-moment"
>
<template #button-content>
<gl-icon name="chevron-down" class="mr-0" />
<span class="sr-only">{{ __('Select merge moment') }}</span>
</template>
<gl-dropdown-item
icon-name="warning"
button-class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
@click="handleMergeImmediatelyButtonClick"
>
{{ __('Merge immediately') }}
</gl-dropdown-item>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
</gl-dropdown>
</gl-button-group>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls">
<label v-if="canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
class="js-remove-source-branch-checkbox"
type="checkbox"
/>
{{ __('Delete source branch') }}
</label>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isSquashReadOnly"
/>
</template>
<template v-else>
<div class="bold js-resolve-mr-widget-items-message">
<div
v-if="hasPipelineMustSucceedConflict"
class="gl-display-flex gl-align-items-center"
data-testid="pipeline-succeed-conflict"
>
<gl-sprintf :message="pipelineMustSucceedConflictText" />
<gl-link
:href="mr.pipelineMustSucceedDocsPath"
target="_blank"
class="gl-display-flex gl-ml-2"
>
<gl-icon name="question" />
</gl-link>
</div>
<gl-sprintf v-else :message="mergeDisabledText" />
</div>
</template>
</div>
</div>
<div v-if="isSHAMismatch" class="d-flex align-items-center mt-2 js-sha-mismatch">
<gl-icon name="warning-solid" class="text-warning mr-1" />
<span class="text-warning">
<gl-sprintf
:message="
__('New changes were added. %{linkStart}Reload the page to review them%{linkEnd}')
"
>
<template #link="{ content }">
<gl-link :href="mr.mergeRequestDiffsPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</div>
</div>
</div>
<commits-header
v-if="shouldShowSquashEdit || shouldShowMergeEdit"
:is-squash-enabled="squashBeforeMerge"
:commits-count="mr.commitsCount"
:target-branch="mr.targetBranch"
:is-fast-forward-enabled="mr.ffOnlyEnabled"
:class="{ 'border-bottom': mr.mergeError }"
>
<ul class="border-top content-list commits-list flex-list">
<commit-edit
v-if="shouldShowSquashEdit"
v-model="squashCommitMessage"
:label="__('Squash commit message')"
input-id="squash-message-edit"
squash
>
<commit-message-dropdown
slot="header"
<merge-train-helper-text
v-if="shouldRenderMergeTrainHelperText"
:pipeline-id="pipeline.id"
:pipeline-link="pipeline.path"
:merge-train-length="stateData.mergeTrainsCount"
:merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
/>
<template v-if="shouldShowMergeControls">
<div v-if="!shouldShowMergeEdit" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
</div>
<commits-header
v-if="shouldShowSquashEdit || shouldShowMergeEdit"
:is-squash-enabled="squashBeforeMerge"
:commits-count="commitsCount"
:target-branch="stateData.targetBranch"
:is-fast-forward-enabled="!shouldShowMergeEdit"
:class="{ 'border-bottom': stateData.mergeError }"
>
<ul class="border-top content-list commits-list flex-list">
<commit-edit
v-if="shouldShowSquashEdit"
v-model="squashCommitMessage"
:commits="mr.commits"
/>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
v-model="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
<label slot="checkbox">
<input
id="include-description"
type="checkbox"
@change="updateMergeCommitMessage($event.target.checked)"
:label="__('Squash commit message')"
input-id="squash-message-edit"
squash
>
<commit-message-dropdown
slot="header"
v-model="squashCommitMessage"
:commits="commits"
/>
{{ __('Include merge request description') }}
</label>
</commit-edit>
</ul>
</commits-header>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
v-model="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
<label slot="checkbox">
<input
id="include-description"
type="checkbox"
@change="updateMergeCommitMessage($event.target.checked)"
/>
{{ __('Include merge request description') }}
</label>
</commit-edit>
</ul>
</commits-header>
</template>
</template>
</div>
</template>

View File

@ -27,7 +27,7 @@ export default {
return __('Merge when pipeline succeeds');
},
shouldShowMergeImmediatelyDropdown() {
return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
return this.isPipelineActive && !this.stateData.onlyAllowMergeIfPipelineSucceeds;
},
isMergeImmediatelyDangerous() {
return false;

View File

@ -0,0 +1,41 @@
fragment ReadyToMerge on Project {
onlyAllowMergeIfPipelineSucceeds
mergeRequestsFfOnlyEnabled
squashReadOnly
mergeRequest(iid: $iid) {
autoMergeEnabled
shouldRemoveSourceBranch
defaultMergeCommitMessage
defaultMergeCommitMessageWithDescription
defaultSquashCommitMessage
squash
squashOnMerge
availableAutoMergeStrategies
hasCi
mergeable
mergeWhenPipelineSucceeds
commitCount
diffHeadSha
userPermissions {
removeSourceBranch
}
targetBranch
mergeError
commitsWithoutMergeCommits {
nodes {
sha
shortId
title
message
}
}
pipelines(first: 1) {
nodes {
id
status
path
active
}
}
}
}

View File

@ -0,0 +1,7 @@
#import "./ready_to_merge.fragment.graphql"
query readyToMergeQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
...ReadyToMerge
}
}

View File

@ -167,7 +167,7 @@ export default class MergeRequestStore {
this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged';
this.canMerge = mergeRequest.userPermissions.canMerge;
this.ciStatus = pipeline?.status.toLowerCase();
this.commitsCount = mergeRequest.commitCount;
this.commitsCount = mergeRequest.commitCount || 10;
this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
this.hasConflicts = mergeRequest.conflicts;
this.hasMergeableDiscussionsState = mergeRequest.mergeableDiscussionsState === false;

View File

@ -28,6 +28,7 @@
@import './pages/profiles/preferences';
@import './pages/projects';
@import './pages/prometheus';
@import './pages/registry';
@import './pages/runners';
@import './pages/search';
@import './pages/service_desk';

View File

@ -1016,3 +1016,11 @@ $mr-widget-min-height: 69px;
vertical-align: middle;
}
}
.mr-ready-to-merge-loader {
max-width: 418px;
> svg {
vertical-align: middle;
}
}

View File

@ -0,0 +1,8 @@
// Workaround for gl-breadcrumb at the last child of the handwritten breadcrumb
// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
//
// See app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue when this is changed.
.breadcrumbs-container .gl-breadcrumbs {
padding: 0;
box-shadow: none;
}

View File

@ -17,10 +17,6 @@
}
}
.registry-placeholder {
min-height: 60px;
}
.auto-devops-card {
margin-bottom: $gl-vert-padding;
}

View File

@ -143,16 +143,3 @@
flex-direction: column !important;
}
}
// These will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1091
.gl-w-10p {
width: 10%;
}
.gl-w-20p {
width: 20%;
}
.gl-w-40p {
width: 40%;
}

View File

@ -96,6 +96,8 @@ module Types
description: 'Default merge commit message of the merge request'
field :default_merge_commit_message_with_description, GraphQL::STRING_TYPE, null: true,
description: 'Default merge commit message of the merge request with description'
field :default_squash_commit_message, GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
description: 'Default squash commit message of the merge request'
field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false,
description: 'Indicates if a merge is currently occurring'
field :source_branch_exists, GraphQL::BOOLEAN_TYPE,
@ -161,6 +163,8 @@ module Types
description: 'Users who approved the merge request'
field :squash_on_merge, GraphQL::BOOLEAN_TYPE, null: false, method: :squash_on_merge?,
description: 'Indicates if squash on merge is enabled'
field :squash, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates if squash on merge is enabled'
field :available_auto_merge_strategies, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true,
description: 'Array of available auto merge strategies'
field :has_ci, GraphQL::BOOLEAN_TYPE, null: false, method: :has_ci?,

View File

@ -3,10 +3,7 @@
- show_invited_members = can_manage_members && @invited_members.exists?
- show_access_requests = can_manage_members && @requesters.exists?
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
- vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group, default_enabled: true)
- filtered_search_enabled = Feature.enabled?(:group_members_filtered_search, @group, default_enabled: true)
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
- form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
.js-remove-member-modal
@ -70,17 +67,9 @@
= render 'groups/group_members/tab_pane/form_item' do
= label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
- if vue_members_list_enabled
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member',
collection: @members, as: :member,
locals: { membership_source: @group,
group: @group,
current_user_is_group_owner: current_user_is_group_owner }
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
.loading
.spinner.spinner-md
= paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
- if @group.shared_with_group_links.any?
#tab-groups.tab-pane
@ -89,14 +78,9 @@
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list{ data: { qa_selector: 'groups_list' } }
- @group.shared_with_group_links.each do |group_link|
= render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_members, group_link_path: group_group_link_path(@group, group_link)
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading
.spinner.spinner-md
- if show_invited_members
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border
@ -106,17 +90,9 @@
= html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
= form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
= render 'shared/members/search_field', name: 'search_invited'
- if vue_members_list_enabled
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list
= render partial: 'shared/members/member',
collection: @invited_members, as: :member,
locals: { membership_source: @group,
group: @group,
current_user_is_group_owner: current_user_is_group_owner }
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
.loading
.spinner.spinner-md
= paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
- if show_access_requests
#tab-access-requests.tab-pane
@ -125,14 +101,6 @@
= render 'groups/group_members/tab_pane/header' do
= render 'groups/group_members/tab_pane/title' do
= html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list
= render partial: 'shared/members/member',
collection: @requesters, as: :member,
locals: { membership_source: @group,
group: @group,
current_user_is_group_owner: current_user_is_group_owner }
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading
.spinner.spinner-md

View File

@ -2,20 +2,18 @@
- @content_class = "limit-container-width" unless fluid_layout
%section
.row.registry-placeholder.prepend-bottom-10
.col-12
#js-container-registry{ data: { endpoint: group_container_registries_path(@group),
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"is_admin": current_user&.admin.to_s,
is_group_page: "true",
"group_path": @group.full_path,
"gid_prefix": container_repository_gid_prefix,
character_error: @character_error.to_s } }
#js-container-registry{ data: { endpoint: group_container_registries_path(@group),
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"is_admin": current_user&.admin.to_s,
is_group_page: "true",
"group_path": @group.full_path,
"gid_prefix": container_repository_gid_prefix,
character_error: @character_error.to_s } }

View File

@ -13,7 +13,7 @@
.form-text.text-muted
- wildcards_url = help_page_url('user/project/protected_branches', anchor: 'wildcard-protected-branches')
- wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url }
= (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported") % { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>', code_tag_start: '<code>', code_tag_end: '</code>' }).html_safe
= (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported.") % { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>', code_tag_start: '<code>', code_tag_end: '</code>' }).html_safe
.form-group.row
%label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
= s_("ProtectedBranch|Allowed to merge:")

View File

@ -7,16 +7,15 @@
%button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
Keep stable branches secure, and force developers to use merge requests. #{link_to "What are protected branches?", help_page_path("user/project/protected_branches")}
.settings-content
%p
By default, protected branches are designed to:
By default, protected branches protect your code and:
%ul
%li prevent their creation, if not already created, from everybody except Maintainers
%li prevent pushes from everybody except Maintainers
%li prevent <strong>anyone</strong> from force pushing to the branch
%li prevent <strong>anyone</strong> from deleting the branch
%p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches")} and #{link_to "project permissions", help_page_path("user/permissions")}.
%li Allow only users with Maintainer #{link_to "permissions", help_page_path("user/permissions")} to create new protected branches.
%li Allow only users with Maintainer permissions to push code.
%li Prevent <strong>anyone</strong> from force-pushing to the branch.
%li Prevent <strong>anyone</strong> from deleting the branch.
- if can? current_user, :admin_project, @project
= content_for :create_protected_branch

View File

@ -2,22 +2,20 @@
- @content_class = "limit-container-width" unless fluid_layout
%section
.row.registry-placeholder.prepend-bottom-10
.col-12
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
expiration_policy: @project.container_expiration_policy.to_json,
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url),
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s,
character_error: @character_error.to_s } }
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
expiration_policy: @project.container_expiration_policy.to_json,
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url),
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s,
character_error: @character_error.to_s } }

View File

@ -127,8 +127,5 @@
= _("Delete")
- unless force_mobile_view
= sprite_icon('remove', css_class: 'd-none d-sm-block gl-icon')
= render_if_exists 'shared/members/ee/override_member_buttons', group: group, member: member, user: user, action: :edit, can_override: member.can_override?
- else
%span.member-access-text.user-access-role= member.human_access
= render_if_exists 'shared/members/ee/override_member_buttons', group: group, member: member, user: user, action: :confirm, can_override: member.can_override?

View File

@ -0,0 +1,5 @@
---
title: Update error message used in boards sidebar subscription
merge_request: 50352
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix UI on global breadcrumb on Project/Group Container Registry
merge_request: 48288
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Add test to check if /users/User is redirected to /User
merge_request: 50651
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Deduplicate labels with identical title and group
merge_request: 37148
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Updated UI text to match style guidelines
merge_request: 50475
author: Amy Qualls @aqualls
type: other

View File

@ -0,0 +1,5 @@
---
title: Avoid 409 StaleObjectError errors with /rebase
merge_request: 50719
author:
type: fixed

View File

@ -1,8 +0,0 @@
---
name: vue_group_members_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40548
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241194
milestone: '13.4'
type: development
group: group::access
default_enabled: true

View File

@ -0,0 +1,135 @@
# frozen_string_literal: true
class RemoveDuplicateLabelsFromGroup < ActiveRecord::Migration[6.0]
DOWNTIME = false
CREATE = 1
RENAME = 2
disable_ddl_transaction!
class BackupLabel < ApplicationRecord
include EachBatch
self.table_name = 'backup_labels'
end
class Label < ApplicationRecord
self.table_name = 'labels'
end
class Group < ApplicationRecord
include EachBatch
self.table_name = 'namespaces'
end
BATCH_SIZE = 10_000
def up
# Split to smaller chunks
# Loop rather than background job, every 10,000
# there are ~1,800,000 groups in total (excluding personal namespaces, which can't have labels)
Group.where(type: 'Group').each_batch(of: BATCH_SIZE) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first
transaction do
remove_full_duplicates(*range)
end
transaction do
rename_partial_duplicates(*range)
end
end
end
DOWN_BATCH_SIZE = 1000
def down
BackupLabel.where('project_id IS NULL AND group_id IS NOT NULL').each_batch(of: DOWN_BATCH_SIZE) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first
restore_renamed_labels(*range)
restore_deleted_labels(*range)
end
end
def remove_full_duplicates(start_id, stop_id)
# Fields that are considered duplicate:
# group_id title template description type color
duplicate_labels = ApplicationRecord.connection.execute(<<-SQL.squish)
WITH data AS (
SELECT labels.*,
row_number() OVER (PARTITION BY labels.group_id, labels.title, labels.template, labels.description, labels.type, labels.color ORDER BY labels.id) AS row_number,
#{CREATE} AS restore_action
FROM labels
WHERE labels.group_id BETWEEN #{start_id} AND #{stop_id}
AND NOT EXISTS (SELECT * FROM board_labels WHERE board_labels.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM label_links WHERE label_links.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM label_priorities WHERE label_priorities.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM lists WHERE lists.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM resource_label_events WHERE resource_label_events.label_id = labels.id)
) SELECT * FROM data WHERE row_number > 1;
SQL
if duplicate_labels.any?
# create backup records
BackupLabel.insert_all!(duplicate_labels.map { |label| label.except("row_number") })
Label.unscoped.where(id: duplicate_labels.pluck("id")).delete_all
end
end
def rename_partial_duplicates(start_id, stop_id)
# We need to ensure that the new title (with `_duplicate#{ID}`) doesn't exceed the limit.
# Truncate the original title (if needed) to 245 characters minus the length of the ID
# then add `_duplicate#{ID}`
soft_duplicates = ApplicationRecord.connection.execute(<<-SQL.squish)
WITH data AS (
SELECT
*,
substring(title from 1 for 245 - length(id::text)) || '_duplicate' || id::text as new_title,
#{RENAME} AS restore_action,
row_number() OVER (PARTITION BY group_id, title ORDER BY id) AS row_number
FROM labels
WHERE group_id BETWEEN #{start_id} AND #{stop_id}
) SELECT * FROM data WHERE row_number > 1;
SQL
if soft_duplicates.any?
# create backup records
BackupLabel.insert_all!(soft_duplicates.map { |label| label.except("row_number") })
ApplicationRecord.connection.execute(<<-SQL.squish)
UPDATE labels SET title = substring(title from 1 for 245 - length(id::text)) || '_duplicate' || id::text
WHERE labels.id IN (#{soft_duplicates.map { |dup| dup["id"] }.join(", ")});
SQL
end
end
def restore_renamed_labels(start_id, stop_id)
# the backup label IDs are not incremental, they are copied directly from the Labels table
ApplicationRecord.connection.execute(<<-SQL.squish)
WITH backups AS (
SELECT id, title
FROM backup_labels
WHERE id BETWEEN #{start_id} AND #{stop_id}
AND restore_action = #{RENAME}
) UPDATE labels SET title = backups.title
FROM backups
WHERE labels.id = backups.id;
SQL
end
def restore_deleted_labels(start_id, stop_id)
ActiveRecord::Base.connection.execute(<<-SQL.squish)
INSERT INTO labels
SELECT id, title, color, group_id, created_at, updated_at, template, description, description_html, type, cached_markdown_version FROM backup_labels
WHERE backup_labels.id BETWEEN #{start_id} AND #{stop_id}
AND backup_labels.project_id IS NULL AND backup_labels.group_id IS NOT NULL
AND backup_labels.restore_action = #{CREATE}
SQL
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddUniquenessIndexToLabelTitleAndGroup < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
GROUP_AND_TITLE = [:group_id, :title]
def up
add_concurrent_index :labels, GROUP_AND_TITLE, where: "labels.project_id IS NULL", unique: true, name: "index_labels_on_group_id_and_title_unique"
remove_concurrent_index :labels, GROUP_AND_TITLE, name: "index_labels_on_group_id_and_title"
end
def down
add_concurrent_index :labels, GROUP_AND_TITLE, where: "labels.project_id IS NULL", unique: false, name: "index_labels_on_group_id_and_title"
remove_concurrent_index :labels, GROUP_AND_TITLE, name: "index_labels_on_group_id_and_title_unique"
end
end

View File

@ -0,0 +1 @@
9683f55a327b9579b9b0b9484dd11a07b7ea4244b126c46e0144662cb25da6bb

View File

@ -0,0 +1 @@
71cd12e553b3acbb665770fe7478365f1f082e2d278c67b166f41461f689aa5e

View File

@ -21804,7 +21804,7 @@ CREATE UNIQUE INDEX index_label_priorities_on_project_id_and_label_id ON label_p
CREATE UNIQUE INDEX index_labels_on_group_id_and_project_id_and_title ON labels USING btree (group_id, project_id, title);
CREATE INDEX index_labels_on_group_id_and_title_with_null_project_id ON labels USING btree (group_id, title) WHERE (project_id IS NULL);
CREATE UNIQUE INDEX index_labels_on_group_id_and_title_unique ON labels USING btree (group_id, title) WHERE (project_id IS NULL);
CREATE INDEX index_labels_on_project_id ON labels USING btree (project_id);

View File

@ -600,6 +600,28 @@ on how to achieve that.
If you use an external container registry, some features associated with the
container registry may be unavailable or have [inherent risks](../../user/packages/container_registry/index.md#use-with-external-container-registries).
For the integration to work, the external registry must be configured to
use a JSON Web Token to authenticate with GitLab. The
[external registry's runtime configuration](https://docs.docker.com/registry/configuration/#token)
**must** have the following entries:
```yaml
auth:
token:
realm: https://gitlab.example.com/jwt/auth
service: container_registry
issuer: gitlab-issuer
rootcertbundle: /root/certs/certbundle
```
Without these entries, the registry logins cannot authenticate with GitLab.
GitLab also remains unaware of
[nested image names](../../user/packages/container_registry/#image-naming-convention)
under the project hierarchy, like
`registry.example.com/group/project/image-name:tag` or
`registry.example.com/group/project/my/image-name:tag`, and only recognizes
`registry.example.com/group/project:tag`.
**Omnibus GitLab**
You can use GitLab as an auth endpoint with an external container registry.
@ -609,18 +631,23 @@ You can use GitLab as an auth endpoint with an external container registry.
```ruby
gitlab_rails['registry_enabled'] = true
gitlab_rails['registry_api_url'] = "http://localhost:5000"
gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
gitlab_rails['registry_issuer'] = "gitlab-issuer"
```
`gitlab_rails['registry_enabled'] = true` is needed to enable GitLab
Container Registry features and authentication endpoint. The GitLab bundled
Container Registry service does not start, even with this enabled.
`gitlab_rails['registry_api_url'] = "http://localhost:5000"` can
carry a different hostname and port depending on where the external registry
is hosted. It must also specify `https` if the external registry is
configured to use TLS.
1. A certificate-key pair is required for GitLab and the external container
registry to communicate securely. You need to create a certificate-key
pair, configuring the external container registry with the public
certificate and configuring GitLab with the private key. To do that, add
the following to `/etc/gitlab/gitlab.rb`:
certificate (`rootcertbundle`) and configuring GitLab with the private key.
To do that, add the following to `/etc/gitlab/gitlab.rb`:
```ruby
# registry['internal_key'] should contain the contents of the custom key
@ -664,7 +691,7 @@ You can use GitLab as an auth endpoint with an external container registry.
api_url: "http://localhost:5000"
path: /var/opt/gitlab/gitlab-rails/shared/registry
key: /var/opt/gitlab/gitlab-rails/certificate.key
issuer: omnibus-gitlab-issuer
issuer: gitlab-issuer
```
1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.

View File

@ -94,11 +94,11 @@ host that GitLab runs. For example, an entry would look like this:
```plaintext
*.example.io. 1800 IN A 192.0.2.1
*.example.io. 1800 IN AAAA 2001::1
*.example.io. 1800 IN AAAA 2001:db8::1
```
Where `example.io` is the domain GitLab Pages is served from,
`192.0.2.1` is the IPv4 address of your GitLab instance, and `2001::1` is the
`192.0.2.1` is the IPv4 address of your GitLab instance, and `2001:db8::1` is the
IPv6 address. If you don't have IPv6, you can omit the AAAA record.
NOTE:
@ -274,11 +274,11 @@ world. Custom domains are supported, but no TLS.
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80']
gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001:db8::2]:80']
```
where `192.0.2.1` is the primary IP address that GitLab is listening to and
`192.0.2.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
`192.0.2.2` and `2001:db8::2` are the secondary IPs the GitLab Pages daemon
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
@ -307,12 +307,12 @@ world. Custom domains and TLS are supported.
pages_nginx['enable'] = false
gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80']
gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001::2]:443']
gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001:db8::2]:80']
gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001:db8::2]:443']
```
where `192.0.2.1` is the primary IP address that GitLab is listening to and
`192.0.2.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
`192.0.2.2` and `2001:db8::2` are the secondary IPs where the GitLab Pages daemon
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).

View File

@ -13754,6 +13754,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
defaultMergeCommitMessageWithDescription: String
"""
Default squash commit message of the merge request
"""
defaultSquashCommitMessage: String
"""
Description of the merge request (Markdown rendered as HTML for caching)
"""
@ -14114,6 +14119,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
sourceProjectId: Int
"""
Indicates if squash on merge is enabled
"""
squash: Boolean!
"""
Indicates if squash on merge is enabled
"""

View File

@ -37758,6 +37758,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "defaultSquashCommitMessage",
"description": "Default squash commit message of the merge request",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Description of the merge request (Markdown rendered as HTML for caching)",
@ -38718,6 +38732,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "squash",
"description": "Indicates if squash on merge is enabled",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "squashOnMerge",
"description": "Indicates if squash on merge is enabled",

View File

@ -2082,6 +2082,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `currentUserTodos` | TodoConnection! | Todos for the current user |
| `defaultMergeCommitMessage` | String | Default merge commit message of the merge request |
| `defaultMergeCommitMessageWithDescription` | String | Default merge commit message of the merge request with description |
| `defaultSquashCommitMessage` | String | Default squash commit message of the merge request |
| `description` | String | Description of the merge request (Markdown rendered as HTML for caching) |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `diffHeadSha` | String | Diff head SHA of the merge request |
@ -2125,6 +2126,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `sourceBranchProtected` | Boolean! | Indicates if the source branch is protected |
| `sourceProject` | Project | Source project of the merge request |
| `sourceProjectId` | Int | ID of the merge request source project |
| `squash` | Boolean! | Indicates if squash on merge is enabled |
| `squashOnMerge` | Boolean! | Indicates if squash on merge is enabled |
| `state` | MergeRequestState! | State of the merge request |
| `subscribed` | Boolean! | Indicates if the currently logged in user is subscribed to this merge request |

View File

@ -195,3 +195,34 @@ Without an explicit name argument, Rails can return a false positive
for `index_exists?`, causing a required index to not be created
properly. By always requiring a name for certain types of indexes, the
chance of error is greatly reduced.
## Temporary indexes
There may be times when an index is only needed temporarily.
For example, in a migration, a column of a table might be conditionally
updated. To query which columns need to be updated within the
[query performance guidelines](query_performance.md), an index is needed that would otherwise
not be used.
In these cases, a temporary index should be considered. To specify a
temporary index:
1. Prefix the index name with `tmp_` and follow the [naming conventions](database/constraint_naming_convention.md) and [requirements for naming indexes](#requirements-for-naming-indexes) for the rest of the name.
1. Create a follow-up issue to remove the index in the next (or future) milestone.
1. Add a comment in the migration mentioning the removal issue.
A temporary migration would look like:
```ruby
INDEX_NAME = 'tmp_index_projects_on_owner_where_emails_disabled'
def up
# Temporary index to be removed in 13.9 https://gitlab.com/gitlab-org/gitlab/-/issues/1234
add_concurrent_index :projects, :creator_id, where: 'emails_disabled = false', name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :projects, INDEX_NAME
end
```

View File

@ -499,7 +499,8 @@ addresses and names, do use:
When including sample URLs in the documentation, use:
- `example.com` when the domain name is generic.
- `gitlab.example.com` when referring to self-managed instances of GitLab.
- `gitlab.example.com` when referring only to self-managed GitLab instances.
Use `gitlab.com` for GitLab SaaS instances.
### Fake tokens
@ -530,6 +531,7 @@ You can use these fake tokens as examples:
|-----------------------|----------|
| above | Try to avoid extra words when referring to an example or table in a documentation page, but if required, use **previously** instead. |
| admin, admin area | Use **administration**, **administrator**, **administer**, or **Admin Area** instead. |
| allow, enable | Try to avoid, unless you are talking about security-related features. For example, instead of "This feature allows you to create a pipeline," use "Use this feature to create a pipeline." This phrasing is more active and is from the user perspective, rather than the person who implemented the feature. [View details](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/a/allow-allows). |
| and/or | Use **or** instead, or another sensible construction. |
| below | Try to avoid extra words when referring to an example or table in a documentation page, but if required, use **following** instead. |
| currently | Do not use when talking about the product or its features. The documentation describes the product as it is today. |

View File

@ -307,6 +307,11 @@ and verifying your application is deployed as a Review App in the Kubernetes
cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
[Cluster environment scope isn't respected](https://gitlab.com/gitlab-org/gitlab/-/issues/20351)
when checking for active Kubernetes clusters. For multi-cluster setup to work with Auto DevOps,
create a fallback cluster with **Cluster environment scope** set to `*`. A new cluster isn't
required. You can use any of the clusters already added.
## Limitations
The following restrictions apply.
@ -481,7 +486,7 @@ that works for this problem. Follow these steps to use the tool in Auto DevOps:
### Error: error initializing: Looks like "https://kubernetes-charts.storage.googleapis.com" is not a valid chart repository or cannot be reached
As [announced in the official CNCF blogpost](https://www.cncf.io/blog/2020/10/07/important-reminder-for-all-helm-users-stable-incubator-repos-are-deprecated-and-all-images-are-changing-location/),
As [announced in the official CNCF blog post](https://www.cncf.io/blog/2020/10/07/important-reminder-for-all-helm-users-stable-incubator-repos-are-deprecated-and-all-images-are-changing-location/),
the stable Helm chart repository was deprecated and removed on November 13th, 2020.
You may encounter this error after that date.
@ -526,7 +531,7 @@ To fix your custom chart:
it's used to verify the integrity of the downloaded dependencies.
You can find more information in
[issue #263778, "Migrate PostgreSQL from stable Helm repo"](https://gitlab.com/gitlab-org/gitlab/-/issues/263778).
[issue #263778, "Migrate PostgreSQL from stable Helm repository"](https://gitlab.com/gitlab-org/gitlab/-/issues/263778).
### Error: release .... failed: timed out waiting for the condition
@ -545,7 +550,7 @@ page of the deployed application on port 5000. If your application isn't configu
to serve anything at the root page, or is configured to run on a specific port
*other* than 5000, this check fails.
If it fails, you should see these failures within the events for the relevant
If it fails, you should see these failures in the events for the relevant
Kubernetes namespace. These events look like the following example:
```plaintext

View File

@ -356,6 +356,9 @@ If you ever need to disable 2FA:
This clears all your two-factor authentication registrations, including mobile
applications and U2F / WebAuthn devices.
Support for disabling 2FA is limited, depending on your subscription level. For more information, see the
[Account Recovery](https://about.gitlab.com/support/#account-recovery) section of our website.
## Personal access tokens
When 2FA is enabled, you can no longer use your normal account password to
@ -393,8 +396,12 @@ a new set of recovery codes with SSH:
1. Run:
```shell
ssh git@gitlab.example.com 2fa_recovery_codes
ssh git@gitlab.com 2fa_recovery_codes
```
NOTE:
On self-managed instances, replace **`gitlab.com`** in the command above
with the GitLab server hostname (`gitlab.example.com`).
1. You are prompted to confirm that you want to generate new codes.
Continuing this process invalidates previously saved codes:

View File

@ -288,7 +288,7 @@ git config --global user.email <your email address>
When signing in to the main GitLab application, a `_gitlab_session` cookie is
set. `_gitlab_session` is cleared client-side when you close your browser
and expires after "Application settings -> Session duration (minutes)"/`session_expire_delay`
(defaults to `10080` minutes = 7 days).
(defaults to `10080` minutes = 7 days) of no activity.
When signing in to the main GitLab application, you can also check the
"Remember me" option which sets the `remember_user_token`
@ -316,7 +316,9 @@ The `remember_user_token` lifetime of a cookie can now extend beyond the deadlin
GitLab uses both session and persistent cookies:
- Session cookie: Session cookies are normally removed at the end of the browser session when the browser is closed. The `_gitlab_session` cookie has no expiration date.
- Session cookie: Session cookies are normally removed at the end of the browser session when
the browser is closed. The `_gitlab_session` cookie has no fixed expiration date. However,
it expires based on its [`session_expire_delay`](#why-do-i-keep-getting-signed-out).
- Persistent cookie: The `remember_user_token` is a cookie with an expiration date of two weeks. GitLab activates this cookie if you click Remember Me when you sign in.
By default, the server sets a time-to-live (TTL) of 1-week on any session that is used.

View File

@ -1,11 +1,12 @@
# frozen_string_literal: true
require_relative 'title_linting'
module Gitlab
module Danger
class BaseLinter
MIN_SUBJECT_WORDS_COUNT = 3
MAX_LINE_LENGTH = 72
WIP_PREFIX = 'WIP: '
attr_reader :commit, :problems
@ -58,7 +59,7 @@ module Gitlab
private
def subject
message_parts[0].delete_prefix(WIP_PREFIX)
TitleLinting.remove_draft_flag(message_parts[0])
end
def subject_too_short?

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require_relative 'title_linting'
module Gitlab
module Danger
module Changelog
@ -75,7 +77,7 @@ module Gitlab
end
def sanitized_mr_title
helper.sanitize_mr_title(gitlab.mr_json["title"])
TitleLinting.sanitize_mr_title(gitlab.mr_json["title"])
end
def categories_need_changelog?

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true
require_relative 'teammate'
require_relative 'title_linting'
module Gitlab
module Danger
module Helper
RELEASE_TOOLS_BOT = 'gitlab-release-tools-bot'
DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
# Returns a list of all files that have been added, modified or renamed.
# `git.modified_files` might contain paths that already have been renamed,
@ -216,14 +216,10 @@ module Gitlab
usernames.map { |u| Gitlab::Danger::Teammate.new('username' => u) }
end
def sanitize_mr_title(title)
title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`')
end
def draft_mr?
return false unless gitlab_helper
DRAFT_REGEX.match?(gitlab_helper.mr_json['title'])
TitleLinting.has_draft_flag?(gitlab_helper.mr_json['title'])
end
def security_mr?

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Gitlab
module Danger
module TitleLinting
DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
module_function
def sanitize_mr_title(title)
remove_draft_flag(title).gsub(/`/, '\\\`')
end
def remove_draft_flag(title)
title.gsub(DRAFT_REGEX, '')
end
def has_draft_flag?(title)
DRAFT_REGEX.match?(title)
end
end
end
end

View File

@ -48,6 +48,7 @@ module Gitlab
condition do
merge_request = quick_action_target
next false unless merge_request.open?
next false unless merge_request.source_branch_exists?
access_check = ::Gitlab::UserAccess
@ -56,6 +57,11 @@ module Gitlab
access_check.can_push_to_branch?(merge_request.source_branch)
end
command :rebase do
if quick_action_target.rebase_in_progress?
@execution_message[:rebase] = _('A rebase is already in progress.')
next
end
# This will be used to avoid simultaneous "/merge" and "/rebase" actions
@updates[:rebase] = true

View File

@ -1325,6 +1325,9 @@ msgstr ""
msgid "A ready-to-go template for use with iOS Swift apps"
msgstr ""
msgid "A rebase is already in progress."
msgstr ""
msgid "A regular expression that will be used to find the test coverage output in the job log. Leave blank to disable"
msgstr ""
@ -3309,9 +3312,6 @@ msgstr ""
msgid "An error occurred while retrieving projects."
msgstr ""
msgid "An error occurred while saving LDAP override status. Please try again."
msgstr ""
msgid "An error occurred while saving assignees"
msgstr ""
@ -15576,7 +15576,7 @@ msgstr ""
msgid "IssueAnalytics|Weight"
msgstr ""
msgid "IssueBoards|An error occurred while setting notifications status."
msgid "IssueBoards|An error occurred while setting notifications status. Please try again."
msgstr ""
msgid "IssueBoards|Board"
@ -22681,7 +22681,7 @@ msgstr ""
msgid "Protected branches"
msgstr ""
msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported"
msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported."
msgstr ""
msgid "ProtectedBranch|Allowed to merge"
@ -22714,7 +22714,7 @@ msgstr ""
msgid "ProtectedBranch|Protected branch (%{protected_branches_count})"
msgstr ""
msgid "ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected"
msgid "ProtectedBranch|Reject code pushes that change files listed in the CODEOWNERS file."
msgstr ""
msgid "ProtectedBranch|Require approval from code owners:"
@ -32005,9 +32005,6 @@ msgstr ""
msgid "You do not have permissions to run the import."
msgstr ""
msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
msgstr ""
msgid "You don't have any U2F devices registered yet."
msgstr ""

View File

@ -44,7 +44,7 @@
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.177.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "25.0.1",
"@gitlab/ui": "25.2.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-3",
"@rails/ujs": "^6.0.3-2",

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
import { GlBanner, GlButton } from '@gitlab/ui';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
@ -107,14 +107,12 @@ describe('InviteMembersBanner', () => {
});
describe('dismissing', () => {
const findButton = () => {
return wrapper.find('button');
};
const findButton = () => wrapper.findAll(GlButton).at(1);
beforeEach(() => {
wrapper = createComponent({ GlBanner });
findButton().trigger('click');
findButton().vm.$emit('click');
});
it('sets iDismissed to true', () => {

View File

@ -1,28 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
<ul>
<li
class="foo bar"
exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
<div
class="gl-breadcrumbs"
>
<ol
class="breadcrumb gl-breadcrumb-list"
>
baz
</li>
<li
class="foo bar"
>
foo
</li>
<!---->
<li>
<a
class="foo"
<li
class="breadcrumb-item gl-breadcrumb-item"
>
<a>
</a>
</a>
</li>
</ul>
<a
class=""
href="/"
target="_self"
/>
</li>
<span
class="gl-breadcrumb-separator"
data-testid="separator"
>
<svg
aria-hidden="true"
class="gl-icon s8"
data-testid="angle-right-icon"
>
<use
href="#angle-right"
/>
</svg>
</span>
<li
class="breadcrumb-item gl-breadcrumb-item"
>
<a
class=""
href="#"
target="_self"
/>
</li>
<!---->
</ol>
</div>
`;
exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
<div
class="gl-breadcrumbs"
>
<ol
class="breadcrumb gl-breadcrumb-list"
>
<li
class="breadcrumb-item gl-breadcrumb-item"
>
<a
class=""
href="/"
target="_self"
/>
</li>
<!---->
</ol>
</div>
`;

View File

@ -1,4 +1,4 @@
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import component from '~/registry/explorer/components/registry_breadcrumb.vue';
@ -6,45 +6,13 @@ describe('Registry Breadcrumb', () => {
let wrapper;
const nameGenerator = jest.fn();
const crumb = {
className: 'foo bar',
tagName: 'div',
innerHTML: 'baz',
querySelector: jest.fn(),
children: [
{
tagName: 'a',
className: 'foo',
},
],
};
const querySelectorReturnValue = {
classList: ['js-divider'],
tagName: 'svg',
innerHTML: 'foo',
};
const crumbs = [crumb, { ...crumb, innerHTML: 'foo' }, { ...crumb, className: 'baz' }];
const routes = [
{ name: 'foo', meta: { nameGenerator, root: true } },
{ name: 'baz', meta: { nameGenerator } },
{ name: 'list', path: '/', meta: { nameGenerator, root: true } },
{ name: 'details', path: '/:id', meta: { nameGenerator } },
];
const findDivider = () => wrapper.find('.js-divider');
const findRootRoute = () => wrapper.find({ ref: 'rootRouteLink' });
const findChildRoute = () => wrapper.find({ ref: 'childRouteLink' });
const findLastCrumb = () => wrapper.find({ ref: 'lastCrumb' });
const mountComponent = ($route) => {
wrapper = shallowMount(component, {
propsData: {
crumbs,
},
stubs: {
'router-link': { name: 'router-link', template: '<a><slot></slot></a>', props: ['to'] },
},
wrapper = mount(component, {
mocks: {
$route,
$router: {
@ -58,7 +26,6 @@ describe('Registry Breadcrumb', () => {
beforeEach(() => {
nameGenerator.mockClear();
crumb.querySelector = jest.fn();
});
afterEach(() => {
@ -75,8 +42,11 @@ describe('Registry Breadcrumb', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('contains a router-link for the child route', () => {
expect(findChildRoute().exists()).toBe(true);
it('contains only a single router-link to list', () => {
const links = wrapper.findAll('a');
expect(links).toHaveLength(1);
expect(links.at(0).attributes('href')).toBe('/');
});
it('the link text is calculated by nameGenerator', () => {
@ -86,48 +56,23 @@ describe('Registry Breadcrumb', () => {
describe('when is not rootRoute', () => {
beforeEach(() => {
crumb.querySelector.mockReturnValue(querySelectorReturnValue);
mountComponent(routes[1]);
});
it('renders a divider', () => {
expect(findDivider().exists()).toBe(true);
it('renders', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('contains a router-link for the root route', () => {
expect(findRootRoute().exists()).toBe(true);
});
it('contains two router-links to list and details', () => {
const links = wrapper.findAll('a');
it('contains a router-link for the child route', () => {
expect(findChildRoute().exists()).toBe(true);
expect(links).toHaveLength(2);
expect(links.at(0).attributes('href')).toBe('/');
expect(links.at(1).attributes('href')).toBe('#');
});
it('the link text is calculated by nameGenerator', () => {
expect(nameGenerator).toHaveBeenCalledTimes(2);
});
});
describe('last crumb', () => {
const lastChildren = crumb.children[0];
beforeEach(() => {
nameGenerator.mockReturnValue('foo');
mountComponent(routes[0]);
});
it('has the same tag as the last children of the crumbs', () => {
expect(findLastCrumb().element.tagName).toBe(lastChildren.tagName.toUpperCase());
});
it('has the same classes as the last children of the crumbs', () => {
expect(findLastCrumb().classes().join(' ')).toEqual(lastChildren.className);
});
it('has a link to the current route', () => {
expect(findChildRoute().props('to')).toEqual({ to: routes[0].name });
});
it('the link has the correct text', () => {
expect(findChildRoute().text()).toEqual('foo');
});
});
});

View File

@ -19,7 +19,7 @@ describe('Submit Changes Error', () => {
});
};
const findRetryButton = () => wrapper.find(GlButton);
const findRetryButton = () => wrapper.findAll(GlButton).at(1);
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {

View File

@ -47,7 +47,8 @@ describe('Commits message dropdown component', () => {
});
it('should have correct message for the first dropdown list element', () => {
expect(findFirstDropdownElement().text()).toBe('78d5b7 Commit 1');
expect(findFirstDropdownElement().text()).toContain('78d5b7');
expect(findFirstDropdownElement().text()).toContain('Commit 1');
});
it('should emit a commit title on selecting commit', () => {

View File

@ -29,7 +29,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
total_time_spent reference author merged_at commit_count current_user_todos
conflicts auto_merge_enabled approved_by source_branch_protected
default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies
has_ci mergeable commits_without_merge_commits security_auto_fix
has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message
]
if Gitlab.ee?

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
require_relative 'danger_spec_helper'
require 'gitlab/danger/base_linter'
@ -70,19 +71,57 @@ RSpec.describe Gitlab::Danger::BaseLinter do
end
end
context 'when subject is a WIP' do
context 'when ignoring length issues for subject having not-ready wording' do
using RSpec::Parameterized::TableSyntax
let(:final_message) { 'A B C' }
# commit message with prefix will be over max length. commit message without prefix will be of maximum size
let(:commit_message) { described_class::WIP_PREFIX + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) }
it 'does not have any problems' do
commit_linter.lint_subject
context 'when used as prefix' do
where(prefix: [
'WIP: ',
'WIP:',
'wIp:',
'[WIP] ',
'[WIP]',
'[draft]',
'[draft] ',
'(draft)',
'(draft) ',
'draft - ',
'draft: ',
'draft:',
'DRAFT:'
])
expect(commit_linter.problems).to be_empty
with_them do
it 'does not have any problems' do
commit_message = prefix + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size)
commit = commit_class.new(commit_message, anything, anything)
linter = described_class.new(commit).lint_subject
expect(linter.problems).to be_empty
end
end
end
context 'when used as suffix' do
where(suffix: %w[WIP draft])
with_them do
it 'does not have any problems' do
commit_message = final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) + suffix
commit = commit_class.new(commit_message, anything, anything)
linter = described_class.new(commit).lint_subject
expect(linter.problems).to be_empty
end
end
end
end
context 'when subject is too short and too long' do
context 'when subject does not have enough words and is too long' do
let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH }
it 'adds a problem' do

View File

@ -150,41 +150,80 @@ RSpec.describe Gitlab::Danger::Changelog do
end
describe '#modified_text' do
let(:sanitize_mr_title) { 'Fake Title' }
let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } }
subject { changelog.modified_text }
it do
expect(subject).to include('CHANGELOG.md was edited')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
context "when title is not changed from sanitization", :aggregate_failures do
let(:sanitize_mr_title) { 'Fake Title' }
specify do
expect(subject).to include('CHANGELOG.md was edited')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
end
end
context "when title needs sanitization", :aggregate_failures do
let(:sanitize_mr_title) { 'DRAFT: Fake Title' }
specify do
expect(subject).to include('CHANGELOG.md was edited')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
end
end
end
describe '#required_text' do
let(:sanitize_mr_title) { 'Fake Title' }
let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } }
subject { changelog.required_text }
it do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).not_to include('--ee')
context "when title is not changed from sanitization", :aggregate_failures do
let(:sanitize_mr_title) { 'Fake Title' }
specify do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).not_to include('--ee')
end
end
context "when title needs sanitization", :aggregate_failures do
let(:sanitize_mr_title) { 'DRAFT: Fake Title' }
specify do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).not_to include('--ee')
end
end
end
describe 'optional_text' do
let(:sanitize_mr_title) { 'Fake Title' }
describe '#optional_text' do
let(:mr_json) { { "iid" => 1234, "title" => sanitize_mr_title } }
subject { changelog.optional_text }
it do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
context "when title is not changed from sanitization", :aggregate_failures do
let(:sanitize_mr_title) { 'Fake Title' }
specify do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
end
end
context "when title needs sanitization", :aggregate_failures do
let(:sanitize_mr_title) { 'DRAFT: Fake Title' }
specify do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
end
end
end
end

View File

@ -402,24 +402,6 @@ RSpec.describe Gitlab::Danger::Helper do
end
end
describe '#sanitize_mr_title' do
where(:mr_title, :expected_mr_title) do
'My MR title' | 'My MR title'
'WIP: My MR title' | 'My MR title'
'Draft: My MR title' | 'My MR title'
'(Draft) My MR title' | 'My MR title'
'[Draft] My MR title' | 'My MR title'
'[DRAFT] My MR title' | 'My MR title'
'DRAFT: My MR title' | 'My MR title'
end
with_them do
subject { helper.sanitize_mr_title(mr_title) }
it { is_expected.to eq(expected_mr_title) }
end
end
describe '#security_mr?' do
it 'returns false when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil)

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
require 'gitlab/danger/title_linting'
RSpec.describe Gitlab::Danger::TitleLinting do
using RSpec::Parameterized::TableSyntax
describe '#sanitize_mr_title' do
where(:mr_title, :expected_mr_title) do
'`My MR title`' | "\\`My MR title\\`"
'WIP: My MR title' | 'My MR title'
'Draft: My MR title' | 'My MR title'
'(Draft) My MR title' | 'My MR title'
'[Draft] My MR title' | 'My MR title'
'[DRAFT] My MR title' | 'My MR title'
'DRAFT: My MR title' | 'My MR title'
'DRAFT: `My MR title`' | "\\`My MR title\\`"
end
with_them do
subject { described_class.sanitize_mr_title(mr_title) }
it { is_expected.to eq(expected_mr_title) }
end
end
describe '#remove_draft_flag' do
where(:mr_title, :expected_mr_title) do
'WIP: My MR title' | 'My MR title'
'Draft: My MR title' | 'My MR title'
'(Draft) My MR title' | 'My MR title'
'[Draft] My MR title' | 'My MR title'
'[DRAFT] My MR title' | 'My MR title'
'DRAFT: My MR title' | 'My MR title'
end
with_them do
subject { described_class.remove_draft_flag(mr_title) }
it { is_expected.to eq(expected_mr_title) }
end
end
describe '#has_draft_flag?' do
it 'returns true for a draft title' do
expect(described_class.has_draft_flag?('Draft: My MR title')).to be true
end
it 'returns false for non draft title' do
expect(described_class.has_draft_flag?('My MR title')).to be false
end
end
end

View File

@ -0,0 +1,227 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200716234259_remove_duplicate_labels_from_group.rb')
RSpec.describe RemoveDuplicateLabelsFromGroup do
let(:labels_table) { table(:labels) }
let(:labels) { labels_table.all }
let(:projects_table) { table(:projects) }
let(:projects) { projects_table.all }
let(:namespaces_table) { table(:namespaces) }
let(:namespaces) { namespaces_table.all }
let(:backup_labels_table) { table(:backup_labels) }
let(:backup_labels) { backup_labels_table.all }
# for those cases where we can't use the activerecord class because the `type` column
# makes it think it has polymorphism and should be/have a Label subclass
let(:sql_backup_labels) { ApplicationRecord.connection.execute('SELECT * from backup_labels') }
# all the possible tables with records that may have a relationship with a label
let(:analytics_cycle_analytics_group_stages_table) { table(:analytics_cycle_analytics_group_stages) }
let(:analytics_cycle_analytics_project_stages_table) { table(:analytics_cycle_analytics_project_stages) }
let(:board_labels_table) { table(:board_labels) }
let(:label_links_table) { table(:label_links) }
let(:label_priorities_table) { table(:label_priorities) }
let(:lists_table) { table(:lists) }
let(:resource_label_events_table) { table(:resource_label_events) }
let!(:group_one) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') }
let!(:project_one) do
projects_table.create!(id: 1, name: 'project', path: 'project',
visibility_level: 0, namespace_id: group_one.id)
end
let(:label_title) { 'bug' }
let(:label_color) { 'red' }
let(:label_description) { 'nice label' }
let(:project_id) { project_one.id }
let(:group_id) { group_one.id }
let(:other_title) { 'feature' }
let(:group_label_attributes) do
{
title: label_title, color: label_color, group_id: group_id, type: 'GroupLabel', template: false, description: label_description
}
end
let(:migration) { described_class.new }
describe 'removing full duplicates' do
context 'when there are no duplicate labels' do
let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1, title: "a different label")) }
let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2, title: "a totally different label")) }
it 'does not remove anything' do
expect { migration.up }.not_to change { backup_labels_table.count }
end
it 'restores removed records when rolling back - no change' do
migration.up
expect { migration.down }.not_to change { labels_table.count }
end
end
context 'with duplicates with no relationships' do
let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1)) }
let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2)) }
let!(:third_label) { labels_table.create!(group_label_attributes.merge(id: 3, title: other_title)) }
let!(:fourth_label) { labels_table.create!(group_label_attributes.merge(id: 4, title: other_title)) }
it 'creates a backup record for each removed record' do
expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
end
it 'creates the correct backup records with `create` restore_action' do
migration.up
expect(sql_backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
expect(sql_backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
end
it 'deletes all but one' do
migration.up
expect { second_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { fourth_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'restores removed records on rollback' do
second_label_attributes = modified_attributes(second_label)
fourth_label_attributes = modified_attributes(fourth_label)
migration.up
migration.down
expect(second_label.attributes).to include(second_label_attributes)
expect(fourth_label.attributes).to include(fourth_label_attributes)
end
end
context 'two duplicate records, one of which has a relationship' do
let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1)) }
let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2)) }
let!(:label_priority) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
it 'does not remove anything' do
expect { migration.up }.not_to change { labels_table.count }
end
it 'does not create a backup record with `create` restore_action' do
expect { migration.up }.not_to change { backup_labels_table.where(restore_action: described_class::CREATE).count }
end
it 'restores removed records when rolling back - no change' do
migration.up
expect { migration.down }.not_to change { labels_table.count }
end
end
context 'multiple duplicates, a subset of which have relationships' do
let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1)) }
let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2)) }
let!(:label_priority_for_second_label) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
let!(:third_label) { labels_table.create!(group_label_attributes.merge(id: 3)) }
let!(:fourth_label) { labels_table.create!(group_label_attributes.merge(id: 4)) }
let!(:label_priority_for_fourth_label) { label_priorities_table.create!(label_id: fourth_label.id, project_id: project_id, priority: 2) }
it 'creates a backup record with `create` restore_action for each removed record' do
expect { migration.up }.to change { backup_labels_table.where(restore_action: described_class::CREATE).count }.from(0).to(1)
end
it 'creates the correct backup records' do
migration.up
expect(sql_backup_labels.find { |bl| bl["id"] == 3 }).to include(third_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
end
it 'deletes the duplicate record' do
migration.up
expect { first_label.reload }.not_to raise_error
expect { second_label.reload }.not_to raise_error
expect { third_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'restores removed records on rollback' do
third_label_attributes = modified_attributes(third_label)
migration.up
migration.down
expect(third_label.attributes).to include(third_label_attributes)
end
end
end
describe 'renaming partial duplicates' do
# partial duplicates - only group_id and title match. Distinct colour prevents deletion.
context 'when there are no duplicate labels' do
let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1, title: "a unique label", color: 'green')) }
let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2, title: "a totally different, unique, label", color: 'blue')) }
it 'does not rename anything' do
expect { migration.up }.not_to change { backup_labels_table.count }
end
end
context 'with duplicates with no relationships' do
let!(:first_label) { labels_table.create!(group_label_attributes.merge(id: 1, color: 'green')) }
let!(:second_label) { labels_table.create!(group_label_attributes.merge(id: 2, color: 'blue')) }
let!(:third_label) { labels_table.create!(group_label_attributes.merge(id: 3, title: other_title, color: 'purple')) }
let!(:fourth_label) { labels_table.create!(group_label_attributes.merge(id: 4, title: other_title, color: 'yellow')) }
it 'creates a backup record for each renamed record' do
expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
end
it 'creates the correct backup records with `rename` restore_action' do
migration.up
expect(sql_backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
expect(sql_backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
end
it 'modifies the titles of the partial duplicates' do
migration.up
expect(second_label.reload.title).to match(/#{label_title}_duplicate#{second_label.id}$/)
expect(fourth_label.reload.title).to match(/#{other_title}_duplicate#{fourth_label.id}$/)
end
it 'restores renamed records on rollback' do
second_label_attributes = modified_attributes(second_label)
fourth_label_attributes = modified_attributes(fourth_label)
migration.up
migration.down
expect(second_label.reload.attributes).to include(second_label_attributes)
expect(fourth_label.reload.attributes).to include(fourth_label_attributes)
end
context 'when the labels have a long title that might overflow' do
let(:long_title) { "a" * 255 }
before do
first_label.update_attribute(:title, long_title)
second_label.update_attribute(:title, long_title)
end
it 'keeps the length within the limit' do
migration.up
expect(second_label.reload.title).to eq("#{"a" * 244}_duplicate#{second_label.id}")
expect(second_label.title.length).to eq(255)
end
end
end
end
def modified_attributes(label)
label.attributes.except('created_at', 'updated_at')
end
end

View File

@ -520,14 +520,29 @@ RSpec.describe API::Labels do
expect(json_response['color']).to eq(label1.color)
end
it 'returns 200 if group label already exists' do
create(:group_label, title: label1.name, group: group)
context 'if group label already exists' do
let!(:group_label) { create(:group_label, title: label1.name, group: group) }
expect { put api("/projects/#{project.id}/labels/promote", user), params: { name: label1.name } }
.to change(project.labels, :count).by(-1)
.and change(group.labels, :count).by(0)
it 'returns a status of 200' do
put api("/projects/#{project.id}/labels/promote", user), params: { name: label1.name }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to have_gitlab_http_status(:ok)
end
it 'does not change the group label count' do
expect { put api("/projects/#{project.id}/labels/promote", user), params: { name: label1.name } }
.not_to change(group.labels, :count)
end
it 'does not change the group label max (reuses the same ID)' do
expect { put api("/projects/#{project.id}/labels/promote", user), params: { name: label1.name } }
.not_to change(group.labels, :max)
end
it 'changes the project label count' do
expect { put api("/projects/#{project.id}/labels/promote", user), params: { name: label1.name } }
.to change(project.labels, :count).by(-1)
end
end
it 'returns 403 if guest promotes label' do

View File

@ -97,6 +97,14 @@ RSpec.describe UsersController do
end
end
describe 'GET /users/:username (deprecated user top)' do
it 'redirects to /user1' do
get '/users/user1'
expect(response).to redirect_to user_path('user1')
end
end
describe 'GET #activity' do
shared_examples_for 'renders the show template' do
it 'renders the show template' do

View File

@ -36,6 +36,33 @@ RSpec.shared_examples 'rebase quick action' do
expect(page).to have_content "Scheduled a rebase of branch #{merge_request.source_branch}."
end
context 'when the merge request is closed' do
before do
merge_request.close!
end
it 'does not rebase the MR', :sidekiq_inline do
add_note("/rebase")
expect(page).not_to have_content 'Scheduled a rebase'
end
end
context 'when a rebase is in progress', :sidekiq_inline, :clean_gitlab_redis_shared_state do
before do
jid = SecureRandom.hex
merge_request.update!(rebase_jid: jid)
Gitlab::SidekiqStatus.set(jid)
end
it 'tells the user a rebase is in progress' do
add_note('/rebase')
expect(page).to have_content 'A rebase is already in progress.'
expect(page).not_to have_content 'Scheduled a rebase'
end
end
end
context 'when the current user cannot rebase the MR' do
@ -48,7 +75,7 @@ RSpec.shared_examples 'rebase quick action' do
it 'does not rebase the MR' do
add_note("/rebase")
expect(page).not_to have_content 'Your commands have been executed!'
expect(page).not_to have_content 'Scheduled a rebase'
end
end
end

View File

@ -871,10 +871,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@25.0.1":
version "25.0.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.0.1.tgz#2669cd65a19cda69af42017f39eb964072c03bf8"
integrity sha512-WLvrnVU18DaPHQghwUbYcqoK02axD639T4THDUBk3fzzOs2piXYvqDfRtT59cpaZpDS3IIHUTlPhxBCE7zv2GQ==
"@gitlab/ui@25.2.1":
version "25.2.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.2.1.tgz#2c332134bbc82a6c40ff5fdb73aacccf730629d8"
integrity sha512-bOkL2sfkovCV6MO/N70Xfe+vTdyi2Vp2efgDvOx4tHzqJllM6Y379wculi0VmdGw3X4TpmPI+zLWAAZ9vkhDAQ==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"