Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ca89460cfa
commit
db3acec198
|
|
@ -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'
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -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'
|
||||
|
|
|
|||
12
Gemfile.lock
12
Gemfile.lock
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#import "./ready_to_merge.fragment.graphql"
|
||||
|
||||
query readyToMergeQuery($projectPath: ID!, $iid: String!) {
|
||||
project(fullPath: $projectPath) {
|
||||
...ReadyToMerge
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1016,3 +1016,11 @@ $mr-widget-min-height: 69px;
|
|||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.mr-ready-to-merge-loader {
|
||||
max-width: 418px;
|
||||
|
||||
> svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -17,10 +17,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.registry-placeholder {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.auto-devops-card {
|
||||
margin-bottom: $gl-vert-padding;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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:")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update error message used in boards sidebar subscription
|
||||
merge_request: 50352
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix UI on global breadcrumb on Project/Group Container Registry
|
||||
merge_request: 48288
|
||||
author: Takuya Noguchi
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add test to check if /users/User is redirected to /User
|
||||
merge_request: 50651
|
||||
author: Takuya Noguchi
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Deduplicate labels with identical title and group
|
||||
merge_request: 37148
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Updated UI text to match style guidelines
|
||||
merge_request: 50475
|
||||
author: Amy Qualls @aqualls
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Avoid 409 StaleObjectError errors with /rebase
|
||||
merge_request: 50719
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
9683f55a327b9579b9b0b9484dd11a07b7ea4244b126c46e0144662cb25da6bb
|
||||
|
|
@ -0,0 +1 @@
|
|||
71cd12e553b3acbb665770fe7478365f1f082e2d278c67b166f41461f689aa5e
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue