Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-16 15:09:45 +00:00
parent 93fb07b8c9
commit 7212129029
66 changed files with 1061 additions and 341 deletions

View File

@ -1,23 +0,0 @@
<!--
Performance Indicator Metric issues are used for adding, updating, or removing performance indicator type in Service Ping metrics.
Please title your issue with the following format: "{action}(Add|Update|Remove) Metric name as performance indicator"
Example of title: "Add some_feature_views as gmau"
-->
## Summary
<!--
Summary of the changes
-->
## Tasks
- [ ] [Link to metric definition]()
- [ ] Create issue in GitLab Data Team project using [Product Performance Indicator template](https://gitlab.com/gitlab-data/analytics/-/issues/new?issuable_template=Product%20Performance%20Indicator%20Template)
See [Product Intelligence Guide](https://docs.gitlab.com/ee/development/service_ping/performance_indicator_metrics.html) for details
/label ~"product intelligence" ~"Data Warehouse::Impact Check"

View File

@ -566,9 +566,6 @@ Graphql/Descriptions:
RSpec/ImplicitSubject: RSpec/ImplicitSubject:
Enabled: false Enabled: false
RSpec/Be:
Enabled: false
RSpec/DescribedClass: RSpec/DescribedClass:
Enabled: false Enabled: false

View File

@ -0,0 +1,22 @@
---
RSpec/Be:
Exclude:
- 'ee/spec/services/groups/transfer_service_spec.rb'
- 'spec/lib/bulk_imports/common/pipelines/boards_pipeline_spec.rb'
- 'spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb'
- 'spec/lib/gitlab/lets_encrypt/client_spec.rb'
- 'spec/lib/gitlab/search_context/builder_spec.rb'
- 'spec/migrations/20220503035221_add_gitlab_schema_to_batched_background_migrations_spec.rb'
- 'spec/models/concerns/issuable_spec.rb'
- 'spec/models/identity_spec.rb'
- 'spec/models/snippet_repository_spec.rb'
- 'spec/presenters/packages/nuget/search_results_presenter_spec.rb'
- 'spec/requests/api/graphql/mutations/snippets/create_spec.rb'
- 'spec/requests/api/pages_domains_spec.rb'
- 'spec/services/pages/delete_service_spec.rb'
- 'spec/services/pages/destroy_deployments_service_spec.rb'
- 'spec/services/pages/migrate_from_legacy_storage_service_spec.rb'
- 'spec/services/projects/update_pages_service_spec.rb'
- 'spec/support/shared_examples/requests/api/packages_shared_examples.rb'
- 'spec/uploaders/file_uploader_spec.rb'
- 'spec/uploaders/namespace_file_uploader_spec.rb'

View File

@ -1,3 +1,7 @@
export const INDEX_ROUTE_NAME = 'index'; export const INDEX_ROUTE_NAME = 'index';
export const NEW_ROUTE_NAME = 'new'; export const NEW_ROUTE_NAME = 'new';
export const EDIT_ROUTE_NAME = 'edit'; export const EDIT_ROUTE_NAME = 'edit';
export const trackViewsOptions = {
category: 'Customer Relations' /* eslint-disable-line @gitlab/require-i18n-strings */,
action: 'view_contacts_list',
};

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CrmContactsRoot from './components/contacts_root.vue'; import CrmContactsRoot from './components/contacts_root.vue';
import routes from './routes'; import routes from './routes';
@ -21,7 +22,14 @@ export default () => {
return false; return false;
} }
const { basePath, groupFullPath, groupIssuesPath, canAdminCrmContact, groupId } = el.dataset; const {
basePath,
groupFullPath,
groupIssuesPath,
canAdminCrmContact,
groupId,
textQuery,
} = el.dataset;
const router = new VueRouter({ const router = new VueRouter({
base: basePath, base: basePath,
@ -33,7 +41,13 @@ export default () => {
el, el,
router, router,
apolloProvider, apolloProvider,
provide: { groupFullPath, groupIssuesPath, canAdminCrmContact, groupId }, provide: {
groupFullPath,
groupIssuesPath,
canAdminCrmContact: parseBoolean(canAdminCrmContact),
groupId,
textQuery,
},
render(createElement) { render(createElement) {
return createElement(CrmContactsRoot); return createElement(CrmContactsRoot);
}, },

View File

@ -57,7 +57,7 @@ export default {
getQuery() { getQuery() {
return { return {
query: getGroupContactsQuery, query: getGroupContactsQuery,
variables: { groupFullPath: this.groupFullPath }, variables: { groupFullPath: this.groupFullPath, ids: [this.contactGraphQLId] },
}; };
}, },
title() { title() {

View File

@ -1,36 +1,54 @@
<script> <script>
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME } from '../../constants'; import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql'; import {
bodyTrClass,
initialPaginationState,
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME, trackViewsOptions } from '../../constants';
import getGroupContacts from './graphql/get_group_contacts.query.graphql';
import getGroupContactsCountByState from './graphql/get_group_contacts_count_by_state.graphql';
export default { export default {
components: { components: {
GlAlert,
GlButton, GlButton,
GlLoadingIcon, GlLoadingIcon,
GlTable, GlTable,
PaginatedTableWithSearchAndTabs,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath'], inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath', 'textQuery'],
data() { data() {
return { return {
contacts: [], contacts: { list: [] },
contactsCount: {},
error: false, error: false,
filteredByStatus: '',
pagination: initialPaginationState,
statusFilter: 'all',
searchTerm: this.textQuery,
sort: 'LAST_NAME_ASC',
sortDesc: false,
}; };
}, },
apollo: { apollo: {
contacts: { contacts: {
query() { query: getGroupContacts,
return getGroupContactsQuery;
},
variables() { variables() {
return { return {
groupFullPath: this.groupFullPath, groupFullPath: this.groupFullPath,
searchTerm: this.searchTerm,
state: this.statusFilter,
sort: this.sort,
firstPageSize: this.pagination.firstPageSize,
lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor,
}; };
}, },
update(data) { update(data) {
@ -40,19 +58,52 @@ export default {
this.error = true; this.error = true;
}, },
}, },
contactsCount: {
query: getGroupContactsCountByState,
variables() {
return {
groupFullPath: this.groupFullPath,
searchTerm: this.searchTerm,
};
},
update(data) {
return data?.group?.contactStateCounts;
},
error() {
this.error = true;
},
},
}, },
computed: { computed: {
isLoading() { isLoading() {
return this.$apollo.queries.contacts.loading; return this.$apollo.queries.contacts.loading;
}, },
canAdmin() { tbodyTrClass() {
return parseBoolean(this.canAdminCrmContact); return {
[bodyTrClass]: !this.loading && !this.isEmpty,
};
}, },
}, },
methods: { methods: {
errorAlertDismissed() {
this.error = true;
},
extractContacts(data) { extractContacts(data) {
const contacts = data?.group?.contacts?.nodes || []; const contacts = data?.group?.contacts?.nodes || [];
return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName)); const pageInfo = data?.group?.contacts?.pageInfo || {};
return {
list: contacts,
pageInfo,
};
},
fetchSortedData({ sortBy, sortDesc }) {
const sortingColumn = convertToSnakeCase(sortBy).toUpperCase();
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
this.pagination = initialPaginationState;
this.sort = `${sortingColumn}_${sortingDirection}`;
},
filtersChanged({ searchTerm }) {
this.searchTerm = searchTerm;
}, },
getIssuesPath(path, value) { getIssuesPath(path, value) {
return `${path}?crm_contact_id=${value}`; return `${path}?crm_contact_id=${value}`;
@ -60,6 +111,13 @@ export default {
getEditRoute(id) { getEditRoute(id) {
return { name: this.$options.EDIT_ROUTE_NAME, params: { id } }; return { name: this.$options.EDIT_ROUTE_NAME, params: { id } };
}, },
pageChanged(pagination) {
this.pagination = pagination;
},
statusChanged({ filters, status }) {
this.statusFilter = filters;
this.filteredByStatus = status;
},
}, },
fields: [ fields: [
{ key: 'firstName', sortable: true }, { key: 'firstName', sortable: true },
@ -92,57 +150,109 @@ export default {
}, },
EDIT_ROUTE_NAME, EDIT_ROUTE_NAME,
NEW_ROUTE_NAME, NEW_ROUTE_NAME,
statusTabs: [
{
title: __('Active'),
status: 'ACTIVE',
filters: 'active',
},
{
title: __('Inactive'),
status: 'INACTIVE',
filters: 'inactive',
},
{
title: __('All'),
status: 'ALL',
filters: 'all',
},
],
trackViewsOptions,
emptyArray: [],
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = false"> <paginated-table-with-search-and-tabs
{{ $options.i18n.errorText }} :show-items="true"
</gl-alert> :show-error-msg="false"
<div :i18n="$options.i18n"
class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6" :items="contacts.list"
:page-info="contacts.pageInfo"
:items-count="contactsCount"
:status-tabs="$options.statusTabs"
:track-views-options="$options.trackViewsOptions"
:filter-search-tokens="$options.emptyArray"
filter-search-key="contacts"
@page-changed="pageChanged"
@tabs-changed="statusChanged"
@filters-changed="filtersChanged"
@error-alert-dismissed="errorAlertDismissed"
> >
<h2 class="gl-font-size-h2 gl-my-0"> <template #header-actions>
{{ $options.i18n.title }} <router-link v-if="canAdminCrmContact" :to="{ name: $options.NEW_ROUTE_NAME }">
</h2> <gl-button class="gl-my-3 gl-mr-5" variant="confirm" data-testid="new-contact-button">
<div v-if="canAdmin">
<router-link :to="{ name: $options.NEW_ROUTE_NAME }">
<gl-button variant="confirm" data-testid="new-contact-button">
{{ $options.i18n.newContact }} {{ $options.i18n.newContact }}
</gl-button> </gl-button>
</router-link> </router-link>
</div>
</div>
<router-view />
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else
class="gl-mt-5"
:items="contacts"
:fields="$options.fields"
:empty-text="$options.i18n.emptyText"
show-empty
>
<template #cell(id)="{ value: id }">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
class="gl-mr-3"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="getIssuesPath(groupIssuesPath, id)"
/>
<router-link :to="getEditRoute(id)">
<gl-button
v-if="canAdmin"
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
data-testid="edit-contact-button"
icon="pencil"
:aria-label="$options.i18n.editButtonLabel"
/>
</router-link>
</template> </template>
</gl-table>
<template #title>
{{ $options.i18n.title }}
</template>
<template #table>
<gl-table
:items="contacts.list"
:fields="$options.fields"
:busy="isLoading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
sort-direction="asc"
:sort-desc.sync="sortDesc"
sort-by="createdAt"
show-empty
no-local-sorting
sort-icon-left
fixed
@sort-changed="fetchSortedData"
>
<template #cell(id)="{ value: id }">
<gl-button
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
class="gl-mr-3"
data-testid="issues-link"
icon="issues"
:aria-label="$options.i18n.issuesButtonLabel"
:href="getIssuesPath(groupIssuesPath, id)"
/>
<router-link :to="getEditRoute(id)">
<gl-button
v-if="canAdminCrmContact"
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
data-testid="edit-contact-button"
icon="pencil"
:aria-label="$options.i18n.editButtonLabel"
/>
</router-link>
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
<template #empty>
<span v-if="error">
{{ $options.i18n.errorText }}
</span>
<span v-else>
{{ $options.i18n.emptyText }}
</span>
</template>
</gl-table>
</template>
</paginated-table-with-search-and-tabs>
<router-view />
</div> </div>
</template> </template>

View File

@ -1,13 +1,38 @@
#import "./crm_contact_fields.fragment.graphql" #import "./crm_contact_fields.fragment.graphql"
query contacts($groupFullPath: ID!) { query contacts(
$groupFullPath: ID!
$state: CustomerRelationsContactState
$searchTerm: String
$sort: ContactSort
$firstPageSize: Int
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
$ids: [CustomerRelationsContactID!]
) {
group(fullPath: $groupFullPath) { group(fullPath: $groupFullPath) {
__typename __typename
id id
contacts { contacts(
state: $state
search: $searchTerm
sort: $sort
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
before: $prevPageCursor
ids: $ids
) {
nodes { nodes {
...ContactFragment ...ContactFragment
} }
pageInfo {
hasNextPage
endCursor
hasPreviousPage
startCursor
}
} }
} }
} }

View File

@ -0,0 +1,11 @@
query contactsCountByState($groupFullPath: ID!, $searchTerm: String) {
group(fullPath: $groupFullPath) {
__typename
id
contactStateCounts(search: $searchTerm) {
all
active
inactive
}
}
}

View File

@ -1,7 +1,6 @@
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlBadge } from '@gitlab/ui'; import { GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
@ -47,11 +46,6 @@ export default {
this.job.coverage, this.job.coverage,
); );
}, },
runnerHelpUrl() {
return helpPagePath('ci/runners/configure_runners.html', {
anchor: 'set-maximum-job-timeout-for-a-runner',
});
},
runnerId() { runnerId() {
const { id, short_sha: token, description } = this.job.runner; const { id, short_sha: token, description } = this.job.runner;
@ -85,6 +79,7 @@ export default {
TAGS: __('Tags:'), TAGS: __('Tags:'),
TIMEOUT: __('Timeout'), TIMEOUT: __('Timeout'),
}, },
RUNNER_HELP_URL: 'https://docs.gitlab.com/runner/register/index.html',
}; };
</script> </script>
@ -101,7 +96,7 @@ export default {
<detail-row v-if="job.queued_duration" :value="queuedDuration" :title="$options.i18n.QUEUED" /> <detail-row v-if="job.queued_duration" :value="queuedDuration" :title="$options.i18n.QUEUED" />
<detail-row <detail-row
v-if="hasTimeout" v-if="hasTimeout"
:help-url="runnerHelpUrl" :help-url="$options.RUNNER_HELP_URL"
:value="timeout" :value="timeout"
data-testid="job-timeout" data-testid="job-timeout"
:title="$options.i18n.TIMEOUT" :title="$options.i18n.TIMEOUT"

View File

@ -28,13 +28,13 @@ export default {
GlSprintf, GlSprintf,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
inject: ['runnerHelpPagePath'],
methods: { methods: {
trackHelpPageClick() { trackHelpPageClick() {
const { label, actions } = pipelineEditorTrackingOptions; const { label, actions } = pipelineEditorTrackingOptions;
this.track(actions.helpDrawerLinks.runners, { label }); this.track(actions.helpDrawerLinks.runners, { label });
}, },
}, },
RUNNER_HELP_URL: 'https://docs.gitlab.com/runner/register/index.html',
}; };
</script> </script>
<template> <template>
@ -47,7 +47,7 @@ export default {
<p class="gl-mb-0"> <p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.note"> <gl-sprintf :message="$options.i18n.note">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="runnerHelpPagePath" target="_blank" @click="trackHelpPageClick()"> <gl-link :href="$options.RUNNER_HELP_URL" target="_blank" @click="trackHelpPageClick()">
{{ content }} {{ content }}
</gl-link> </gl-link>
</template> </template>

View File

@ -40,7 +40,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectFullPath, projectFullPath,
projectPath, projectPath,
projectNamespace, projectNamespace,
runnerHelpPagePath,
simulatePipelineHelpPagePath, simulatePipelineHelpPagePath,
totalBranches, totalBranches,
validateTabIllustrationPath, validateTabIllustrationPath,
@ -132,7 +131,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectFullPath, projectFullPath,
projectPath, projectPath,
projectNamespace, projectNamespace,
runnerHelpPagePath,
simulatePipelineHelpPagePath, simulatePipelineHelpPagePath,
totalBranches: parseInt(totalBranches, 10), totalBranches: parseInt(totalBranches, 10),
validateTabIllustrationPath, validateTabIllustrationPath,

View File

@ -156,6 +156,10 @@ export default {
handleToggle() { handleToggle() {
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
}, },
addButtonClick(event) {
this.isOpen = true;
this.$emit('toggleAddRelatedIssuesForm', event);
},
}, },
linkedIssueTypesTextMap, linkedIssueTypesTextMap,
}; };
@ -203,7 +207,7 @@ export default {
:aria-label="addIssuableButtonText" :aria-label="addIssuableButtonText"
:class="qaClass" :class="qaClass"
class="gl-ml-3" class="gl-ml-3"
@click="$emit('toggleAddRelatedIssuesForm', $event)" @click="addButtonClick"
> >
<slot name="add-button-text">{{ __('Add') }}</slot> <slot name="add-button-text">{{ __('Add') }}</slot>
</gl-button> </gl-button>

View File

@ -88,7 +88,10 @@ export default {
.then( .then(
({ ({
data: { data: {
issuableSetConfidential: { errors }, issuableSetConfidential: {
issuable: { confidential },
errors,
},
}, },
}) => { }) => {
if (errors.length) { if (errors.length) {
@ -96,7 +99,7 @@ export default {
message: errors[0], message: errors[0],
}); });
} else { } else {
this.$emit('closeForm'); this.$emit('closeForm', { confidential });
} }
}, },
) )

View File

@ -95,10 +95,10 @@ export default {
confidentialWidget.setConfidentiality = null; confidentialWidget.setConfidentiality = null;
}, },
methods: { methods: {
closeForm() { closeForm({ confidential } = {}) {
this.$refs.editable.collapse(); this.$refs.editable.collapse();
this.$el.dispatchEvent(hideDropdownEvent); this.$el.dispatchEvent(hideDropdownEvent);
this.$emit('closeForm'); this.$emit('closeForm', { confidential });
}, },
// synchronizing the quick action with the sidebar widget // synchronizing the quick action with the sidebar widget
// this is a temporary solution until we have confidentiality real-time updates // this is a temporary solution until we have confidentiality real-time updates

View File

@ -39,6 +39,7 @@ import SidebarTimeTracking from './components/time_tracking/sidebar_time_trackin
import { IssuableAttributeType } from './constants'; import { IssuableAttributeType } from './constants';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
import CrmContacts from './components/crm_contacts/crm_contacts.vue'; import CrmContacts from './components/crm_contacts/crm_contacts.vue';
import SidebarEventHub from './event_hub';
Vue.use(Translate); Vue.use(Translate);
Vue.use(VueApollo); Vue.use(VueApollo);
@ -359,6 +360,13 @@ function mountConfidentialComponent() {
? IssuableType.Issue ? IssuableType.Issue
: IssuableType.MergeRequest, : IssuableType.MergeRequest,
}, },
on: {
closeForm({ confidential }) {
if (confidential !== undefined) {
SidebarEventHub.$emit('confidentialityUpdated', confidential);
}
},
},
}), }),
}); });
} }

View File

@ -7,6 +7,8 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { isMetaKey } from '~/lib/utils/common_utils'; import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import SidebarEventHub from '~/sidebar/event_hub';
import { import {
STATE_OPEN, STATE_OPEN,
WIDGET_ICONS, WIDGET_ICONS,
@ -111,7 +113,16 @@ export default {
return this.isLoading && this.children.length === 0 ? '...' : this.children.length; return this.isLoading && this.children.length === 0 ? '...' : this.children.length;
}, },
}, },
mounted() {
SidebarEventHub.$on('confidentialityUpdated', this.refetchWorkItems);
},
destroyed() {
SidebarEventHub.$off('confidentialityUpdated', this.refetchWorkItems);
},
methods: { methods: {
refetchWorkItems() {
this.$apollo.queries.workItem.refetch();
},
badgeVariant(state) { badgeVariant(state) {
return state === STATE_OPEN ? 'success' : 'info'; return state === STATE_OPEN ? 'success' : 'info';
}, },
@ -122,6 +133,7 @@ export default {
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
}, },
showAddForm() { showAddForm() {
this.isOpen = true;
this.isShownAddForm = true; this.isShownAddForm = true;
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus(); this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus();

View File

@ -93,6 +93,11 @@ module Types
field :merge_status_enum, ::Types::MergeRequests::MergeStatusEnum, field :merge_status_enum, ::Types::MergeRequests::MergeStatusEnum,
method: :public_merge_status, null: true, method: :public_merge_status, null: true,
description: 'Merge status of the merge request.' description: 'Merge status of the merge request.'
field :detailed_merge_status, ::Types::MergeRequests::DetailedMergeStatusEnum, method: :detailed_merge_status, null: true,
calls_gitaly: true,
description: 'Detailed merge status of the merge request.', alpha: { milestone: '15.3' }
field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true, field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
calls_gitaly: true, calls_gitaly: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.' description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.'

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Types
module MergeRequests
class DetailedMergeStatusEnum < BaseEnum
graphql_name 'DetailedMergeStatus'
description 'Detailed representation of whether a GitLab merge request can be merged.'
value 'UNCHECKED',
value: :unchecked,
description: 'Merge status has not been checked.'
value 'CHECKING',
value: :checking,
description: 'Currently checking for mergeability.'
value 'MERGEABLE',
value: :mergeable,
description: 'Branch can be merged.'
value 'BROKEN_STATUS',
value: :broken_status,
description: 'Can not merge the source into the target branch, potential conflict.'
value 'CI_MUST_PASS',
value: :ci_must_pass,
description: 'Pipeline must succeed before merging.'
value 'DISCUSSIONS_NOT_RESOLVED',
value: :discussions_not_resolved,
description: 'Discussions must be resolved before merging.'
value 'DRAFT_STATUS',
value: :draft_status,
description: 'Merge request must not be draft before merging.'
value 'NOT_OPEN',
value: :not_open,
description: 'Merge request must be open before merging.'
value 'NOT_APPROVED',
value: :not_approved,
description: 'Merge request must be approved before merging.'
value 'BLOCKED_STATUS',
value: :merge_request_blocked,
description: 'Merge request is blocked by another merge request.'
value 'POLICIES_DENIED',
value: :policies_denied,
description: 'There are denied policies for the merge request.'
end
end
end

View File

@ -32,7 +32,6 @@ module Ci
"project-path" => project.path, "project-path" => project.path,
"project-full-path" => project.full_path, "project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path, "project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/index'),
"simulate-pipeline-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'simulate-a-cicd-pipeline'), "simulate-pipeline-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'simulate-a-cicd-pipeline'),
"total-branches" => total_branches, "total-branches" => total_branches,
"validate-tab-illustration-path" => image_path('illustrations/project-run-CICD-pipelines-sm.svg'), "validate-tab-illustration-path" => image_path('illustrations/project-run-CICD-pipelines-sm.svg'),

View File

@ -1187,17 +1187,30 @@ class MergeRequest < ApplicationRecord
] ]
end end
def detailed_merge_status
if cannot_be_merged_rechecking? || preparing? || checking?
return :checking
elsif unchecked?
return :unchecked
end
checks = execute_merge_checks
if checks.success?
:mergeable
else
checks.failure_reason
end
end
# rubocop: disable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false) def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
if Feature.enabled?(:improved_mergeability_checks, self.project) if Feature.enabled?(:improved_mergeability_checks, self.project)
additional_checks = MergeRequests::Mergeability::RunChecksService.new( additional_checks = execute_merge_checks(params: {
merge_request: self, skip_ci_check: skip_ci_check,
params: { skip_discussions_check: skip_discussions_check
skip_ci_check: skip_ci_check, })
skip_discussions_check: skip_discussions_check additional_checks.execute.success?
}
)
additional_checks.execute.all?(&:success?)
else else
return false unless open? return false unless open?
return false if draft? return false if draft?
@ -2059,6 +2072,12 @@ class MergeRequest < ApplicationRecord
def report_type_enabled?(report_type) def report_type_enabled?(report_type)
!!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type) !!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type)
end end
def execute_merge_checks(params: {})
# rubocop: disable CodeReuse/ServiceClass
MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: params).execute
# rubocop: enable CodeReuse/ServiceClass
end
end end
MergeRequest.prepend_mod_with('MergeRequest') MergeRequest.prepend_mod_with('MergeRequest')

View File

@ -24,12 +24,12 @@ module MergeRequests
private private
def success(*args) def success(**args)
Gitlab::MergeRequests::Mergeability::CheckResult.success(*args) Gitlab::MergeRequests::Mergeability::CheckResult.success(payload: args)
end end
def failure(*args) def failure(**args)
Gitlab::MergeRequests::Mergeability::CheckResult.failed(*args) Gitlab::MergeRequests::Mergeability::CheckResult.failed(payload: args)
end end
end end
end end

View File

@ -4,7 +4,7 @@ module MergeRequests
class CheckBrokenStatusService < CheckBaseService class CheckBrokenStatusService < CheckBaseService
def execute def execute
if merge_request.broken? if merge_request.broken?
failure failure(reason: failure_reason)
else else
success success
end end
@ -17,6 +17,12 @@ module MergeRequests
def cacheable? def cacheable?
false false
end end
private
def failure_reason
:broken_status
end
end end
end end
end end

View File

@ -6,7 +6,7 @@ module MergeRequests
if merge_request.mergeable_ci_state? if merge_request.mergeable_ci_state?
success success
else else
failure failure(reason: failure_reason)
end end
end end
@ -17,6 +17,12 @@ module MergeRequests
def cacheable? def cacheable?
false false
end end
private
def failure_reason
:ci_must_pass
end
end end
end end
end end

View File

@ -6,7 +6,7 @@ module MergeRequests
if merge_request.mergeable_discussions_state? if merge_request.mergeable_discussions_state?
success success
else else
failure failure(reason: failure_reason)
end end
end end
@ -17,6 +17,12 @@ module MergeRequests
def cacheable? def cacheable?
false false
end end
private
def failure_reason
:discussions_not_resolved
end
end end
end end
end end

View File

@ -5,7 +5,7 @@ module MergeRequests
class CheckDraftStatusService < CheckBaseService class CheckDraftStatusService < CheckBaseService
def execute def execute
if merge_request.draft? if merge_request.draft?
failure failure(reason: failure_reason)
else else
success success
end end
@ -18,6 +18,12 @@ module MergeRequests
def cacheable? def cacheable?
false false
end end
private
def failure_reason
:draft_status
end
end end
end end
end end

View File

@ -7,7 +7,7 @@ module MergeRequests
if merge_request.open? if merge_request.open?
success success
else else
failure failure(reason: failure_reason)
end end
end end
@ -18,6 +18,12 @@ module MergeRequests
def cacheable? def cacheable?
false false
end end
private
def failure_reason
:not_open
end
end end
end end
end end

View File

@ -10,36 +10,50 @@ module MergeRequests
end end
def execute def execute
merge_request.mergeability_checks.each_with_object([]) do |check_class, results| @results = merge_request.mergeability_checks.each_with_object([]) do |check_class, result_hash|
check = check_class.new(merge_request: merge_request, params: params) check = check_class.new(merge_request: merge_request, params: params)
next if check.skip? next if check.skip?
check_result = run_check(check) check_result = run_check(check)
results << check_result result_hash << check_result
break results if check_result.failed? break result_hash if check_result.failed?
end end
self
end
def success?
raise 'Execute needs to be called before' if results.nil?
results.all?(&:success?)
end
def failure_reason
raise 'Execute needs to be called before' if results.nil?
results.find(&:failed?)&.payload&.fetch(:reason)
end end
private private
attr_reader :merge_request, :params attr_reader :merge_request, :params, :results
def run_check(check) def run_check(check)
return check.execute unless Feature.enabled?(:mergeability_caching, merge_request.project) return check.execute unless Feature.enabled?(:mergeability_caching, merge_request.project)
return check.execute unless check.cacheable? return check.execute unless check.cacheable?
cached_result = results.read(merge_check: check) cached_result = cached_results.read(merge_check: check)
return cached_result if cached_result.respond_to?(:status) return cached_result if cached_result.respond_to?(:status)
check.execute.tap do |result| check.execute.tap do |result|
results.write(merge_check: check, result_hash: result.to_hash) cached_results.write(merge_check: check, result_hash: result.to_hash)
end end
end end
def results def cached_results
strong_memoize(:results) do strong_memoize(:cached_results) do
Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request) Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request)
end end
end end

View File

@ -22,7 +22,7 @@
.form-group .form-group
= f.label :shared_runners_text, _('Shared runners details'), class: 'label-bold' = f.label :shared_runners_text, _('Shared runners details'), class: 'label-bold'
= f.text_area :shared_runners_text, class: 'form-control gl-form-input', rows: 4 = f.text_area :shared_runners_text, class: 'form-control gl-form-input', rows: 4
.form-text.text-muted= _("Add a custom message with details about the instance's shared runners. The message is visible in group and project CI/CD settings, in the Runners section. Markdown is supported.") .form-text.text-muted= _("Add a custom message with details about the instance's shared runners. The message is visible when you view runners for projects and groups. Markdown is supported.")
.form-group .form-group
= f.label :max_artifacts_size, _('Maximum artifacts size (MB)'), class: 'label-bold' = f.label :max_artifacts_size, _('Maximum artifacts size (MB)'), class: 'label-bold'
= f.number_field :max_artifacts_size, class: 'form-control gl-form-input' = f.number_field :max_artifacts_size, class: 'form-control gl-form-input'

View File

@ -5,4 +5,4 @@
= content_for :after_content do = content_for :after_content do
#js-crm-form-portal #js-crm-form-portal
#js-crm-contacts-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group), group_id: @group.id, can_admin_crm_contact: can?(current_user, :admin_crm_contact, @group).to_s, base_path: group_crm_contacts_path(@group) } } #js-crm-contacts-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group), group_id: @group.id, can_admin_crm_contact: can?(current_user, :admin_crm_contact, @group).to_s, base_path: group_crm_contacts_path(@group), text_query: params[:search] } }

View File

@ -0,0 +1,16 @@
- name: "Redis 5 deprecated" # (required) The name of the feature to be deprecated
announcement_milestone: "15.3" # (required) The milestone when this feature was first announced as deprecated.
announcement_date: "2022-08-22" # (required) The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
removal_date: "2023-05-22" # (required) The date of the milestone release when this feature is planned to be removed. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
reporter: tnir
stage: Enablement
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331468
body: | # (required) Do not modify this line, instead modify the lines below.
With GitLab 13.9, in the Omnibus GitLab package and GitLab Helm chart 4.9, the Redis version [was updated to Redis 6](https://about.gitlab.com/releases/2021/02/22/gitlab-13-9-released/#omnibus-improvements).
Redis 5 has reached the end of life in April 2022 and will no longer be supported as of GitLab 15.6.
If you are using your own Redis 5.0 instance, you should upgrade it to Redis 6.0 or higher before upgrading to GitLab 16.0 or higher.
end_of_support_milestone: "15.6"
end_of_support_date: "2022-11-22"
documentation_url: https://docs.gitlab.com/ee/install/requirements.html

View File

@ -13528,6 +13528,7 @@ Maven metadata.
| <a id="mergerequestdefaultsquashcommitmessage"></a>`defaultSquashCommitMessage` | [`String`](#string) | Default squash commit message of the merge request. | | <a id="mergerequestdefaultsquashcommitmessage"></a>`defaultSquashCommitMessage` | [`String`](#string) | Default squash commit message of the merge request. |
| <a id="mergerequestdescription"></a>`description` | [`String`](#string) | Description of the merge request (Markdown rendered as HTML for caching). | | <a id="mergerequestdescription"></a>`description` | [`String`](#string) | Description of the merge request (Markdown rendered as HTML for caching). |
| <a id="mergerequestdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. | | <a id="mergerequestdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
| <a id="mergerequestdetailedmergestatus"></a>`detailedMergeStatus` **{warning-solid}** | [`DetailedMergeStatus`](#detailedmergestatus) | **Introduced** in 15.3. This feature is in Alpha. It can be changed or removed at any time. Detailed merge status of the merge request. |
| <a id="mergerequestdiffheadsha"></a>`diffHeadSha` | [`String`](#string) | Diff head SHA of the merge request. | | <a id="mergerequestdiffheadsha"></a>`diffHeadSha` | [`String`](#string) | Diff head SHA of the merge request. |
| <a id="mergerequestdiffrefs"></a>`diffRefs` | [`DiffRefs`](#diffrefs) | References of the base SHA, the head SHA, and the start SHA for this merge request. | | <a id="mergerequestdiffrefs"></a>`diffRefs` | [`DiffRefs`](#diffrefs) | References of the base SHA, the head SHA, and the start SHA for this merge request. |
| <a id="mergerequestdiffstatssummary"></a>`diffStatsSummary` | [`DiffStatsSummary`](#diffstatssummary) | Summary of which files were changed in this merge request. | | <a id="mergerequestdiffstatssummary"></a>`diffStatsSummary` | [`DiffStatsSummary`](#diffstatssummary) | Summary of which files were changed in this merge request. |
@ -19580,6 +19581,24 @@ Mutation event of a design within a version.
| <a id="designversioneventmodification"></a>`MODIFICATION` | A modification event. | | <a id="designversioneventmodification"></a>`MODIFICATION` | A modification event. |
| <a id="designversioneventnone"></a>`NONE` | No change. | | <a id="designversioneventnone"></a>`NONE` | No change. |
### `DetailedMergeStatus`
Detailed representation of whether a GitLab merge request can be merged.
| Value | Description |
| ----- | ----------- |
| <a id="detailedmergestatusblocked_status"></a>`BLOCKED_STATUS` | Merge request is blocked by another merge request. |
| <a id="detailedmergestatusbroken_status"></a>`BROKEN_STATUS` | Can not merge the source into the target branch, potential conflict. |
| <a id="detailedmergestatuschecking"></a>`CHECKING` | Currently checking for mergeability. |
| <a id="detailedmergestatusci_must_pass"></a>`CI_MUST_PASS` | Pipeline must succeed before merging. |
| <a id="detailedmergestatusdiscussions_not_resolved"></a>`DISCUSSIONS_NOT_RESOLVED` | Discussions must be resolved before merging. |
| <a id="detailedmergestatusdraft_status"></a>`DRAFT_STATUS` | Merge request must not be draft before merging. |
| <a id="detailedmergestatusmergeable"></a>`MERGEABLE` | Branch can be merged. |
| <a id="detailedmergestatusnot_approved"></a>`NOT_APPROVED` | Merge request must be approved before merging. |
| <a id="detailedmergestatusnot_open"></a>`NOT_OPEN` | Merge request must be open before merging. |
| <a id="detailedmergestatuspolicies_denied"></a>`POLICIES_DENIED` | There are denied policies for the merge request. |
| <a id="detailedmergestatusunchecked"></a>`UNCHECKED` | Merge status has not been checked. |
### `DiffPositionType` ### `DiffPositionType`
Type of file the position refers to. Type of file the position refers to.

View File

@ -447,7 +447,7 @@ You can also use `workflow::ready for review` label. That means that your merge
When your merge request receives an approval from the first reviewer it can be passed to a maintainer. You should default to choosing a maintainer with [domain expertise](#domain-experts), and otherwise follow the Reviewer Roulette recommendation or use the label `ready for merge`. When your merge request receives an approval from the first reviewer it can be passed to a maintainer. You should default to choosing a maintainer with [domain expertise](#domain-experts), and otherwise follow the Reviewer Roulette recommendation or use the label `ready for merge`.
Sometimes, a maintainer may not be available for review. They could be out of the office or [at capacity](#review-response-slo). Sometimes, a maintainer may not be available for review. They could be out of the office or [at capacity](https://about.gitlab.com/handbook/engineering/workflow/code-review/#review-response-slo).
You can and should check the maintainer's availability in their profile. If the maintainer recommended by You can and should check the maintainer's availability in their profile. If the maintainer recommended by
the roulette is not available, choose someone else from that list. the roulette is not available, choose someone else from that list.
@ -679,42 +679,6 @@ Enterprise Edition instance. This has some implications:
Ensure that we support object storage for any file storage we need to perform. For more Ensure that we support object storage for any file storage we need to perform. For more
information, see the [uploads documentation](uploads/index.md). information, see the [uploads documentation](uploads/index.md).
### Review turnaround time
Because [unblocking others is always a top priority](https://about.gitlab.com/handbook/values/#global-optimization),
reviewers are expected to review merge requests in a timely manner,
even when this may negatively impact their other tasks and priorities.
Doing so allows everyone involved in the merge request to iterate faster as the
context is fresh in memory, and improves contributors' experience significantly.
#### Review-response SLO
To ensure swift feedback to ready-to-review code, we maintain a `Review-response` Service-level Objective (SLO). The SLO is defined as:
> Review-response SLO = (time when first review is provided) - (time MR is assigned to reviewer) < 2 business days
If you don't think you can review a merge request in the `Review-response` SLO
time frame, let the author know as soon as possible in the comments
(no later than 36 hours after first receiving the review request)
and try to help them find another reviewer or maintainer who is able to, so that they can be unblocked
and get on with their work quickly. Remove yourself as a reviewer.
If you think you are at capacity and are unable to accept any more reviews until
some have been completed, communicate this through your GitLab status by setting
the 🔴 `:red_circle:` emoji and mentioning that you are at capacity in the status
text. This guides contributors to pick a different reviewer, helping us to
meet the SLO.
Of course, if you are out of office and have
[communicated](https://about.gitlab.com/handbook/paid-time-off/#communicating-your-time-off)
this through your GitLab.com Status, authors are expected to realize this and
find a different reviewer themselves.
When a merge request author has been blocked for longer than
the `Review-response` SLO, they are free to remind the reviewer through Slack or add
another reviewer.
### Customer critical merge requests ### Customer critical merge requests
A merge request may benefit from being considered a customer critical priority because there is a significant benefit to the business in doing so. A merge request may benefit from being considered a customer critical priority because there is a significant benefit to the business in doing so.

View File

@ -233,6 +233,77 @@ class CopyColumnUsingBackgroundMigrationJob < BatchedMigrationJob
end end
``` ```
### Additional filters
By default, when creating background jobs to perform the migration, batched background migrations
iterate over the full specified table. This iteration is done using the
[`PrimaryKeyBatchingStrategy`](https://gitlab.com/gitlab-org/gitlab/-/blob/c9dabd1f4b8058eece6d8cb4af95e9560da9a2ee/lib/gitlab/database/migrations/batched_background_migration_helpers.rb#L17). If the table has 1000 records
and the batch size is 100, the work is batched into 10 jobs. For illustrative purposes,
`EachBatch` is used like this:
```ruby
# PrimaryKeyBatchingStrategy
Namespace.each_batch(of: 100) do |relation|
relation.where(type: nil).update_all(type: 'User') # this happens in each background job
end
```
In some cases, only a subset of records must be examined. If only 10% of the 1000 records
need examination, apply a filter to the initial relation when the jobs are created:
```ruby
Namespace.where(type: nil).each_batch(of: 100) do |relation|
relation.update_all(type: 'User')
end
```
In the first example, we don't know how many records will be updated in each batch.
In the second (filtered) example, we know exactly 100 will be updated with each batch.
`BatchedMigrationJob` provides a `scope_to` helper method to apply additional filters and achieve this:
1. Create a new migration job class that inherits from `BatchedMigrationJob` and defines the additional filter:
```ruby
class BackfillNamespaceType < BatchedMigrationJob
scope_to ->(relation) { relation.where(type: nil) }
def perform
each_sub_batch(operation_name: :update_all) do |sub_batch|
sub_batch.update_all(type: 'User')
end
end
end
```
1. In the post-deployment migration, enqueue the batched background migration:
```ruby
class BackfillNamespaceType < Gitlab::Database::Migration[2.0]
MIGRATION = 'BackfillNamespaceType'
DELAY_INTERVAL = 2.minutes
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(
MIGRATION,
:namespaces,
:id,
job_interval: DELAY_INTERVAL
)
end
def down
delete_batched_background_migration(MIGRATION, :namespaces, :id, [])
end
end
```
NOTE:
When applying additional filters, it is important to ensure they are properly covered by an index to optimize `EachBatch` performance.
In the example above we need an index on `(type, id)` to support the filters. See [the `EachBatch` docs for more information](../iterating_tables_in_batches.md).
## Example ## Example
The `routes` table has a `source_type` field that's used for a polymorphic relationship. The `routes` table has a `source_type` field that's used for a polymorphic relationship.
@ -419,76 +490,8 @@ module Gitlab
end end
``` ```
### Adding filters to the initial batching NOTE:
[Additional filters](#additional-filters) defined with `scope_to` will be ignored by `LooseIndexScanBatchingStrategy` and `distinct_each_batch`.
By default, when creating background jobs to perform the migration, batched background migrations will iterate over the full specified table. This is done using the [`PrimaryKeyBatchingStrategy`](https://gitlab.com/gitlab-org/gitlab/-/blob/c9dabd1f4b8058eece6d8cb4af95e9560da9a2ee/lib/gitlab/database/migrations/batched_background_migration_helpers.rb#L17). This means if there are 1000 records in the table and the batch size is 100, there will be 10 jobs. For illustrative purposes, `EachBatch` is used like this:
```ruby
# PrimaryKeyBatchingStrategy
Projects.all.each_batch(of: 100) do |relation|
relation.where(foo: nil).update_all(foo: 'bar') # this happens in each background job
end
```
There are cases where we only need to look at a subset of records. Perhaps we only need to update 1 out of every 10 of those 1000 records. It would be best if we could apply a filter to the initial relation when the jobs are created:
```ruby
Projects.where(foo: nil).each_batch(of: 100) do |relation|
relation.update_all(foo: 'bar')
end
```
In the `PrimaryKeyBatchingStrategy` example, we do not know how many records will be updated in each batch. In the filtered example, we know exactly 100 will be updated with each batch.
The `PrimaryKeyBatchingStrategy` contains [a method that can be overwritten](https://gitlab.com/gitlab-org/gitlab/-/blob/dd1e70d3676891025534dc4a1e89ca9383178fe7/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb#L38-52) to apply additional filtering on the initial `EachBatch`.
We can accomplish this by:
1. Create a new class that inherits from `PrimaryKeyBatchingStrategy` and overrides the method using the desired filter (this may be the same filter used in the sub-batch):
```ruby
# frozen_string_literal: true
module GitLab
module BackgroundMigration
module BatchingStrategies
class FooStrategy < PrimaryKeyBatchingStrategy
def apply_additional_filters(relation, job_arguments: [], job_class: nil)
relation.where(foo: nil)
end
end
end
end
end
```
1. In the post-deployment migration that queues the batched background migration, specify the new batching strategy using the `batch_class_name` parameter:
```ruby
class BackfillProjectsFoo < Gitlab::Database::Migration[2.0]
MIGRATION = 'BackfillProjectsFoo'
DELAY_INTERVAL = 2.minutes
BATCH_CLASS_NAME = 'FooStrategy'
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
queue_batched_background_migration(
MIGRATION,
:routes,
:id,
job_interval: DELAY_INTERVAL,
batch_class_name: BATCH_CLASS_NAME
)
end
def down
delete_batched_background_migration(MIGRATION, :routes, :id, [])
end
end
```
When applying a batching strategy, it is important to ensure the filter properly covered by an index to optimize `EachBatch` performance. See [the `EachBatch` docs for more information](iterating_tables_in_batches.md).
## Testing ## Testing

View File

@ -95,7 +95,7 @@ are three times as likely to be picked by the [Danger bot](../dangerbot.md) as o
## What to do if you feel overwhelmed ## What to do if you feel overwhelmed
Similar to all types of reviews, [unblocking others is always a top priority](https://about.gitlab.com/handbook/values/#global-optimization). Similar to all types of reviews, [unblocking others is always a top priority](https://about.gitlab.com/handbook/values/#global-optimization).
Database reviewers are expected to [review assigned merge requests in a timely manner](../code_review.md#review-turnaround-time) Database reviewers are expected to [review assigned merge requests in a timely manner](https://about.gitlab.com/handbook/engineering/workflow/code-review/#review-turnaround-time)
or let the author know as soon as possible and help them find another reviewer or maintainer. or let the author know as soon as possible and help them find another reviewer or maintainer.
We are doing reviews to help the rest of the GitLab team and, at the same time, get exposed We are doing reviews to help the rest of the GitLab team and, at the same time, get exposed

View File

@ -10,8 +10,7 @@ This guide describes how to use metrics definitions to define [performance indic
To use a metric definition to manage a performance indicator: To use a metric definition to manage a performance indicator:
1. Create a new issue and use the [Performance Indicator Metric issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Performance%20Indicator%20Metric). 1. Create a merge request that includes related changes.
1. Use labels `~"product intelligence"`, `"~Data Warehouse::Impact Check"`. 1. Use labels `~"product intelligence"`, `"~Data Warehouse::Impact Check"`.
1. Create a merge request that includes changes related only to the metric performance indicator.
1. Update the metric definition `performance_indicator_type` [field](metrics_dictionary.md#metrics-definition-and-validation). 1. Update the metric definition `performance_indicator_type` [field](metrics_dictionary.md#metrics-definition-and-validation).
1. Create an issue in GitLab Data Team project with the [Product Performance Indicator template](https://gitlab.com/gitlab-data/analytics/-/issues/new?issuable_template=Product%20Performance%20Indicator%20Template). 1. Create an issue in GitLab Product Data Insights project with the [PI Chart Help template](https://gitlab.com/gitlab-data/product-analytics/-/issues/new?issuable_template=PI%20Chart%20Help) to have the new metric visualized.

View File

@ -36,14 +36,12 @@ Here are some problems with current issues usage and why we are looking into wor
differences in common interactions that the user needs to hold a complicated mental differences in common interactions that the user needs to hold a complicated mental
model of how they each behave. model of how they each behave.
- Issues are not extensible enough to support all of the emerging jobs they need to facilitate. - Issues are not extensible enough to support all of the emerging jobs they need to facilitate.
- Codebase maintainability and feature development become bigger challenges as we grow the Issue type - Codebase maintainability and feature development becomes a bigger challenge as we grow the Issue type.
beyond its core role of issue tracking into supporting the different work item types and handling beyond its core role of issue tracking into supporting the different work item types and handling
logic and structure differences. logic and structure differences.
- New functionality is typically implemented with first class objects that import behavior from issues via - New functionality is typically implemented with first class objects that import behavior from issues via
shared concerns. This leads to duplicated effort and ultimately small differences between common interactions. This shared concerns. This leads to duplicated effort and ultimately small differences between common interactions. This
leads to inconsistent UX. leads to inconsistent UX.
- Codebase maintainability and feature development becomes a bigger challenges as we grow issues
beyond its core role of issue tracking into supporting the different types and subtle differences between them.
## Work item terminology ## Work item terminology

View File

@ -37,7 +37,7 @@ Each time you push a change, Git records it as a unique *commit*. These commits
the history of when and how a file changed, and who changed it. the history of when and how a file changed, and who changed it.
```mermaid ```mermaid
graph LR graph TB
subgraph Repository commit history subgraph Repository commit history
A(Author: Alex<br>Date: 3 Jan at 1PM<br>Commit message: Added sales figures for January<br> Commit ID: 123abc12) ---> B A(Author: Alex<br>Date: 3 Jan at 1PM<br>Commit message: Added sales figures for January<br> Commit ID: 123abc12) ---> B
B(Author: Sam<br>Date: 4 Jan at 10AM<br>Commit message: Removed outdated marketing information<br> Commit ID: aabb1122) ---> C B(Author: Sam<br>Date: 4 Jan at 10AM<br>Commit message: Removed outdated marketing information<br> Commit ID: aabb1122) ---> C
@ -54,7 +54,7 @@ of a repository are in a default branch. To make changes, you:
1. When you're ready, *merge* your branch into the default branch. 1. When you're ready, *merge* your branch into the default branch.
```mermaid ```mermaid
flowchart LR flowchart TB
subgraph Default branch subgraph Default branch
A[Commit] --> B[Commit] --> C[Commit] --> D[Commit] A[Commit] --> B[Commit] --> C[Commit] --> D[Commit]
end end

View File

@ -91,6 +91,23 @@ The [**Maximum number of active pipelines per project** limit](https://docs.gitl
- [**Pipelines rate limits**](https://docs.gitlab.com/ee/user/admin_area/settings/rate_limit_on_pipelines_creation.html). - [**Pipelines rate limits**](https://docs.gitlab.com/ee/user/admin_area/settings/rate_limit_on_pipelines_creation.html).
- [**Total number of jobs in currently active pipelines**](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#set-cicd-limits). - [**Total number of jobs in currently active pipelines**](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#set-cicd-limits).
</div>
<div class="deprecation removal-160 breaking-change">
### Redis 5 deprecated
End of Support: GitLab <span class="removal-milestone">15.6</span> (2022-11-22)
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
With GitLab 13.9, in the Omnibus GitLab package and GitLab Helm chart 4.9, the Redis version [was updated to Redis 6](https://about.gitlab.com/releases/2021/02/22/gitlab-13-9-released/#omnibus-improvements).
Redis 5 has reached the end of life in April 2022 and will no longer be supported as of GitLab 15.6.
If you are using your own Redis 5.0 instance, you should upgrade it to Redis 6.0 or higher before upgrading to GitLab 16.0 or higher.
</div> </div>
</div> </div>

View File

@ -1844,7 +1844,7 @@ API Security uses the specified media types in the OpenAPI document to generate
To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/get-help/). To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/get-help/).
The [GitLab issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues) is the right place for bugs and feature proposals about API Security and API Fuzzing. The [GitLab issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues) is the right place for bugs and feature proposals about API Security and API Fuzzing.
Please use `~"Category:API Security"` [label](../../../development/contributing/issue_workflow.md#labels) when opening a new issue regarding API fuzzing to ensure it is quickly reviewed by the right people. Please refer to our [review response SLO](../../../development/code_review.md#review-response-slo) to understand when you should receive a response. Please use `~"Category:API Security"` [label](../../../development/contributing/issue_workflow.md#labels) when opening a new issue regarding API fuzzing to ensure it is quickly reviewed by the right people. Please refer to our [review response SLO](https://about.gitlab.com/handbook/engineering/workflow/code-review/#review-response-slo) to understand when you should receive a response.
[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues) for similar entries before submitting your own, there's a good chance somebody else had the same issue or feature proposal. Show your support with an award emoji and or join the discussion. [Search the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues) for similar entries before submitting your own, there's a good chance somebody else had the same issue or feature proposal. Show your support with an award emoji and or join the discussion.

View File

@ -1654,7 +1654,7 @@ API Security uses the specified media types in the OpenAPI document to generate
To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/get-help/). To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/get-help/).
The [GitLab issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues) is the right place for bugs and feature proposals about API Security and DAST API. The [GitLab issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues) is the right place for bugs and feature proposals about API Security and DAST API.
Please use `~"Category:API Security"` [label](../../../development/contributing/issue_workflow.md#labels) when opening a new issue regarding DAST API to ensure it is quickly reviewed by the right people. Please refer to our [review response SLO](../../../development/code_review.md#review-response-slo) to understand when you should receive a response. Please use `~"Category:API Security"` [label](../../../development/contributing/issue_workflow.md#labels) when opening a new issue regarding DAST API to ensure it is quickly reviewed by the right people. Please refer to our [review response SLO](https://about.gitlab.com/handbook/engineering/workflow/code-review/#review-response-slo) to understand when you should receive a response.
[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues) for similar entries before submitting your own, there's a good chance somebody else had the same issue or feature proposal. Show your support with an award emoji and or join the discussion. [Search the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues) for similar entries before submitting your own, there's a good chance somebody else had the same issue or feature proposal. Show your support with an award emoji and or join the discussion.

View File

@ -47,6 +47,28 @@ graph TD
end end
``` ```
## View subgroups of a group
Prerequisite:
- To view private nested subgroups, you must be a direct or inherited member of
the private subgroup.
To view the subgroups of a group:
1. On the top bar, select **Menu > Groups** and find your group.
1. Select the **Subgroups and projects** tab.
1. To view a nested subgroup, expand a subgroup in the hierarchy list.
### Private subgroups in public parent groups
In the hierarchy list, public groups with a private subgroup have an expand option (**{chevron-down}**)
for all users that indicate there is a subgroup. When users who are not direct or inherited members of
the private subgroup select expand (**{chevron-down}**), the nested subgroup does not display.
If you prefer to keep information about the presence of nested subgroups private, we advise that you only
add private subgroups to private parent groups.
## Create a subgroup ## Create a subgroup
Prerequisites: Prerequisites:

View File

@ -16,9 +16,26 @@ to automatically manage your container registry usage.
## Check Container Registry Storage Use ## Check Container Registry Storage Use
The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages, The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages.
which doesn't include the Container Registry. To track work on this, see the epic This page includes the Container Registry usage but is currently only available on GitLab.com.
[Storage management for the Container Registry](https://gitlab.com/groups/gitlab-org/-/epics/7226). Measuring usage is only possible on the new version of the GitLab Container Registry backed by a
metadata database. We are completing the [upgrade and migration of the GitLab.com Container Registry](https://gitlab.com/groups/gitlab-org/-/epics/5523)
first and only then will work on [making this available to self-managed installs](https://gitlab.com/groups/gitlab-org/-/epics/5521).
Image layers stored in the Container Registry are deduplicated at the root namespace level.
Therefore, if you tag the same 500MB image twice (either in the same repository or across distinct
repositories under the same root namespace), it will only count towards the root namespace usage
once. Similarly, if a given image layer is shared across multiple images, be those under the same
container repository, project, group, or across different ones, that layer will count only once
towards the root namespace usage.
Only layers that are referenced by tagged images are accounted for. Untagged images and any layers
referenced exclusively by them are subject to [online garbage collection](index.md#delete-images)
and automatically deleted after 24 hours if they remain unreferenced during that period.
Image layers are stored on the storage backend in the original (usually compressed) format. This
means that the measured size for any given image layer should match the size displayed on the
corresponding [image manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest).
## Cleanup policy ## Cleanup policy

View File

@ -157,6 +157,9 @@ Supported values are:
| `line` | ![Insights example stacked bar chart](img/insights_example_line_chart.png) | | `line` | ![Insights example stacked bar chart](img/insights_example_line_chart.png) |
| `stacked-bar` | ![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png) | | `stacked-bar` | ![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png) |
NOTE:
The `dora` data source only supports the `bar` chart type.
### `query` ### `query`
`query` allows to define the data source and various filtering conditions for the chart. `query` allows to define the data source and various filtering conditions for the chart.
@ -349,7 +352,7 @@ dora:
title: "DORA charts" title: "DORA charts"
charts: charts:
- title: "DORA deployment frequency" - title: "DORA deployment frequency"
type: bar type: bar # only bar chart is supported at the moment
query: query:
data_source: dora data_source: dora
params: params:
@ -372,7 +375,7 @@ dora:
period_limit: 30 period_limit: 30
``` ```
#### `query.metric` ##### `query.metric`
Defines which DORA metric to query. The available values are: Defines which DORA metric to query. The available values are:
@ -398,7 +401,7 @@ Define how far the metrics are queried in the past (default: 15). Maximum lookba
##### `query.environment_tiers` ##### `query.environment_tiers`
An array of environments to include into the calculation (default: production). An array of environments to include into the calculation (default: production). Available options: `production`, `staging`, `testing`, `development`, `other`.
### `projects` ### `projects`

View File

@ -150,7 +150,8 @@ considered equivalent to rebasing.
### Rebase without CI/CD pipeline ### Rebase without CI/CD pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) in GitLab 14.7. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) in GitLab 14.7 [with a flag](../../../../administration/feature_flags.md) named `rebase_without_ci_ui`. Disabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/350262) in GitLab 15.3. Feature flag `rebase_without_ci_ui` removed.
To rebase a merge request's branch without triggering a CI/CD pipeline, select To rebase a merge request's branch without triggering a CI/CD pipeline, select
**Rebase without pipeline** from the merge request reports section. **Rebase without pipeline** from the merge request reports section.

View File

@ -24,6 +24,14 @@ module Gitlab
@connection = connection @connection = connection
end end
def self.generic_instance(batch_table:, batch_column:, job_arguments: [], connection:)
new(
batch_table: batch_table, batch_column: batch_column,
job_arguments: job_arguments, connection: connection,
start_id: 0, end_id: 0, sub_batch_size: 0, pause_ms: 0
)
end
def self.job_arguments_count def self.job_arguments_count
0 0
end end
@ -40,6 +48,16 @@ module Gitlab
end end
end end
def self.scope_to(scope)
define_method(:filter_batch) do |relation|
instance_exec(relation, &scope)
end
end
def filter_batch(relation)
relation
end
def perform def perform
raise NotImplementedError, "subclasses of #{self.class.name} must implement #{__method__}" raise NotImplementedError, "subclasses of #{self.class.name} must implement #{__method__}"
end end
@ -55,9 +73,10 @@ module Gitlab
def each_sub_batch(operation_name: :default, batching_arguments: {}, batching_scope: nil) def each_sub_batch(operation_name: :default, batching_arguments: {}, batching_scope: nil)
all_batching_arguments = { column: batch_column, of: sub_batch_size }.merge(batching_arguments) all_batching_arguments = { column: batch_column, of: sub_batch_size }.merge(batching_arguments)
parent_relation = parent_batch_relation(batching_scope) relation = filter_batch(base_relation)
sub_batch_relation = filter_sub_batch(relation, batching_scope)
parent_relation.each_batch(**all_batching_arguments) do |relation| sub_batch_relation.each_batch(**all_batching_arguments) do |relation|
batch_metrics.instrument_operation(operation_name) do batch_metrics.instrument_operation(operation_name) do
yield relation yield relation
end end
@ -67,9 +86,13 @@ module Gitlab
end end
def distinct_each_batch(operation_name: :default, batching_arguments: {}) def distinct_each_batch(operation_name: :default, batching_arguments: {})
if base_relation != filter_batch(base_relation)
raise 'distinct_each_batch can not be used when additional filters are defined with scope_to'
end
all_batching_arguments = { column: batch_column, of: sub_batch_size }.merge(batching_arguments) all_batching_arguments = { column: batch_column, of: sub_batch_size }.merge(batching_arguments)
parent_batch_relation.distinct_each_batch(**all_batching_arguments) do |relation| base_relation.distinct_each_batch(**all_batching_arguments) do |relation|
batch_metrics.instrument_operation(operation_name) do batch_metrics.instrument_operation(operation_name) do
yield relation yield relation
end end
@ -78,13 +101,15 @@ module Gitlab
end end
end end
def parent_batch_relation(batching_scope = nil) def base_relation
parent_relation = define_batchable_model(batch_table, connection: connection) define_batchable_model(batch_table, connection: connection)
.where(batch_column => start_id..end_id) .where(batch_column => start_id..end_id)
end
return parent_relation unless batching_scope def filter_sub_batch(relation, batching_scope = nil)
return relation unless batching_scope
batching_scope.call(parent_relation) batching_scope.call(relation)
end end
end end
end end

View File

@ -24,6 +24,14 @@ module Gitlab
quoted_column_name = model_class.connection.quote_column_name(column_name) quoted_column_name = model_class.connection.quote_column_name(column_name)
relation = model_class.where("#{quoted_column_name} >= ?", batch_min_value) relation = model_class.where("#{quoted_column_name} >= ?", batch_min_value)
if job_class
relation = filter_batch(relation,
table_name: table_name, column_name: column_name,
job_class: job_class, job_arguments: job_arguments
)
end
relation = apply_additional_filters(relation, job_arguments: job_arguments, job_class: job_class) relation = apply_additional_filters(relation, job_arguments: job_arguments, job_class: job_class)
next_batch_bounds = nil next_batch_bounds = nil
@ -36,13 +44,27 @@ module Gitlab
next_batch_bounds next_batch_bounds
end end
# Deprecated
#
# Use `scope_to` to define additional filters on the migration job class.
#
# see https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#adding-additional-filters.
def apply_additional_filters(relation, job_arguments: [], job_class: nil) def apply_additional_filters(relation, job_arguments: [], job_class: nil)
if job_class.respond_to?(:batching_scope)
return job_class.batching_scope(relation, job_arguments: job_arguments)
end
relation relation
end end
private
def filter_batch(relation, table_name:, column_name:, job_class:, job_arguments: [])
return relation unless job_class.respond_to?(:generic_instance)
job = job_class.generic_instance(
batch_table: table_name, batch_column: column_name,
job_arguments: job_arguments, connection: connection
)
job.filter_batch(relation)
end
end end
end end
end end

View File

@ -13,11 +13,11 @@ module Gitlab
end end
def self.success(payload: {}) def self.success(payload: {})
new(status: SUCCESS_STATUS, payload: default_payload.merge(payload)) new(status: SUCCESS_STATUS, payload: default_payload.merge(**payload))
end end
def self.failed(payload: {}) def self.failed(payload: {})
new(status: FAILED_STATUS, payload: default_payload.merge(payload)) new(status: FAILED_STATUS, payload: default_payload.merge(**payload))
end end
def self.from_hash(data) def self.from_hash(data)

View File

@ -2145,7 +2145,7 @@ msgstr ""
msgid "Add a confidential internal note to this %{noteableDisplayName}." msgid "Add a confidential internal note to this %{noteableDisplayName}."
msgstr "" msgstr ""
msgid "Add a custom message with details about the instance's shared runners. The message is visible in group and project CI/CD settings, in the Runners section. Markdown is supported." msgid "Add a custom message with details about the instance's shared runners. The message is visible when you view runners for projects and groups. Markdown is supported."
msgstr "" msgstr ""
msgid "Add a general comment to this %{noteableDisplayName}." msgid "Add a general comment to this %{noteableDisplayName}."
@ -16799,8 +16799,8 @@ msgstr ""
msgid "From %{providerTitle}" msgid "From %{providerTitle}"
msgstr "" msgstr ""
msgid "From October 19, 2022, free groups will be limited to %d member" msgid "From October 19, 2022, free private groups will be limited to %d member"
msgid_plural "From October 19, 2022, free groups will be limited to %d members" msgid_plural "From October 19, 2022, free private groups will be limited to %d members"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""

View File

@ -1,14 +1,16 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import ContactsRoot from '~/crm/contacts/components/contacts_root.vue'; import ContactsRoot from '~/crm/contacts/components/contacts_root.vue';
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql'; import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
import getGroupContactsCountByStateQuery from '~/crm/contacts/components/graphql/get_group_contacts_count_by_state.graphql';
import routes from '~/crm/contacts/routes'; import routes from '~/crm/contacts/routes';
import { getGroupContactsQueryResponse } from './mock_data'; import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import { getGroupContactsQueryResponse, getGroupContactsCountQueryResponse } from './mock_data';
describe('Customer relations contacts root app', () => { describe('Customer relations contacts root app', () => {
Vue.use(VueApollo); Vue.use(VueApollo);
@ -21,24 +23,30 @@ describe('Customer relations contacts root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName }); const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link'); const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewContactButton = () => wrapper.findByTestId('new-contact-button'); const findNewContactButton = () => wrapper.findByTestId('new-contact-button');
const findError = () => wrapper.findComponent(GlAlert); const findTable = () => wrapper.findComponent(PaginatedTableWithSearchAndTabs);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse); const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
const successCountQueryHandler = jest.fn().mockResolvedValue(getGroupContactsCountQueryResponse);
const basePath = '/groups/flightjs/-/crm/contacts'; const basePath = '/groups/flightjs/-/crm/contacts';
const mountComponent = ({ const mountComponent = ({
queryHandler = successQueryHandler, queryHandler = successQueryHandler,
mountFunction = shallowMountExtended, countQueryHandler = successCountQueryHandler,
canAdminCrmContact = true, canAdminCrmContact = true,
textQuery = null,
} = {}) => { } = {}) => {
fakeApollo = createMockApollo([[getGroupContactsQuery, queryHandler]]); fakeApollo = createMockApollo([
wrapper = mountFunction(ContactsRoot, { [getGroupContactsQuery, queryHandler],
[getGroupContactsCountByStateQuery, countQueryHandler],
]);
wrapper = mountExtended(ContactsRoot, {
router, router,
provide: { provide: {
groupFullPath: 'flightjs', groupFullPath: 'flightjs',
groupId: 26, groupId: 26,
groupIssuesPath: '/issues', groupIssuesPath: '/issues',
canAdminCrmContact, canAdminCrmContact,
textQuery,
}, },
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
}); });
@ -58,9 +66,33 @@ describe('Customer relations contacts root app', () => {
router = null; router = null;
}); });
it('should render loading spinner', () => { it('should render table with default props and loading state', () => {
mountComponent(); mountComponent();
expect(findTable().props()).toMatchObject({
items: [],
itemsCount: {},
pageInfo: {},
statusTabs: [
{ title: 'Active', status: 'ACTIVE', filters: 'active' },
{ title: 'Inactive', status: 'INACTIVE', filters: 'inactive' },
{ title: 'All', status: 'ALL', filters: 'all' },
],
showItems: true,
showErrorMsg: false,
trackViewsOptions: { category: 'Customer Relations', action: 'view_contacts_list' },
i18n: {
emptyText: 'No contacts found',
issuesButtonLabel: 'View issues',
editButtonLabel: 'Edit',
title: 'Customer relations contacts',
newContact: 'New contact',
errorText: 'Something went wrong. Please try again.',
},
serverErrorMessage: '',
filterSearchKey: 'contacts',
filterSearchTokens: [],
});
expect(findLoadingIcon().exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
}); });
@ -83,7 +115,7 @@ describe('Customer relations contacts root app', () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
await waitForPromises(); await waitForPromises();
expect(findError().exists()).toBe(true); expect(wrapper.text()).toContain('Something went wrong. Please try again.');
}); });
}); });
@ -92,11 +124,11 @@ describe('Customer relations contacts root app', () => {
mountComponent(); mountComponent();
await waitForPromises(); await waitForPromises();
expect(findError().exists()).toBe(false); expect(wrapper.text()).not.toContain('Something went wrong. Please try again.');
}); });
it('renders correct results', async () => { it('renders correct results', async () => {
mountComponent({ mountFunction: mountExtended }); mountComponent();
await waitForPromises(); await waitForPromises();
expect(findRowByName(/Marty/i)).toHaveLength(1); expect(findRowByName(/Marty/i)).toHaveLength(1);
@ -105,7 +137,7 @@ describe('Customer relations contacts root app', () => {
const issueLink = findIssuesLinks().at(0); const issueLink = findIssuesLinks().at(0);
expect(issueLink.exists()).toBe(true); expect(issueLink.exists()).toBe(true);
expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=16'); expect(issueLink.attributes('href')).toBe('/issues?crm_contact_id=12');
}); });
}); });
}); });

View File

@ -43,6 +43,28 @@ export const getGroupContactsQueryResponse = {
organization: null, organization: null,
}, },
], ],
pageInfo: {
__typename: 'PageInfo',
hasNextPage: false,
endCursor: 'eyJsYXN0X25hbWUiOiJMZWRuZXIiLCJpZCI6IjE3OSJ9',
hasPreviousPage: false,
startCursor: 'eyJsYXN0X25hbWUiOiJCYXJ0b24iLCJpZCI6IjE5MyJ9',
},
},
},
},
};
export const getGroupContactsCountQueryResponse = {
data: {
group: {
__typename: 'Group',
id: 'gid://gitlab/Group/26',
contactStateCounts: {
all: 241,
active: 239,
inactive: 2,
__typename: 'ContactStateCountsType',
}, },
}, },
}, },

View File

@ -7,7 +7,7 @@ describe('Sidebar detail row', () => {
const title = 'this is the title'; const title = 'this is the title';
const value = 'this is the value'; const value = 'this is the value';
const helpUrl = '/help/ci/runners/index.html'; const helpUrl = 'https://docs.gitlab.com/runner/register/index.html';
const findHelpLink = () => wrapper.findComponent(GlLink); const findHelpLink = () => wrapper.findComponent(GlLink);

View File

@ -8,16 +8,8 @@ describe('First pipeline card', () => {
let wrapper; let wrapper;
let trackingSpy; let trackingSpy;
const defaultProvide = {
runnerHelpPagePath: '/help/runners',
};
const createComponent = () => { const createComponent = () => {
wrapper = mount(FirstPipelineCard, { wrapper = mount(FirstPipelineCard);
provide: {
...defaultProvide,
},
});
}; };
const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }); const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name });
@ -43,7 +35,7 @@ describe('First pipeline card', () => {
}); });
it('renders the link', () => { it('renders the link', () => {
expect(findRunnersLink().href).toContain(defaultProvide.runnerHelpPagePath); expect(findRunnersLink().href).toBe(wrapper.vm.$options.RUNNER_HELP_URL);
}); });
describe('tracking', () => { describe('tracking', () => {

View File

@ -71,7 +71,12 @@ describe('Sidebar Confidentiality Form', () => {
it('creates a flash if mutation contains errors', async () => { it('creates a flash if mutation contains errors', async () => {
createComponent({ createComponent({
mutate: jest.fn().mockResolvedValue({ mutate: jest.fn().mockResolvedValue({
data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } }, data: {
issuableSetConfidential: {
issuable: { confidential: false },
errors: ['Houston, we have a problem!'],
},
},
}), }),
}); });
findConfidentialToggle().vm.$emit('click', new MouseEvent('click')); findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
@ -82,6 +87,24 @@ describe('Sidebar Confidentiality Form', () => {
}); });
}); });
it('emits `closeForm` event with confidentiality value when mutation is successful', async () => {
createComponent({
mutate: jest.fn().mockResolvedValue({
data: {
issuableSetConfidential: {
issuable: { confidential: true },
errors: [],
},
},
}),
});
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
await waitForPromises();
expect(wrapper.emitted('closeForm')).toEqual([[{ confidential: true }]]);
});
describe('when issue is not confidential', () => { describe('when issue is not confidential', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();

View File

@ -132,6 +132,7 @@ describe('Sidebar Confidentiality Widget', () => {
it('closes the form and dispatches an event when `closeForm` is emitted', async () => { it('closes the form and dispatches an event when `closeForm` is emitted', async () => {
createComponent(); createComponent();
const el = wrapper.vm.$el; const el = wrapper.vm.$el;
const closeFormPayload = { confidential: true };
jest.spyOn(el, 'dispatchEvent'); jest.spyOn(el, 'dispatchEvent');
await waitForPromises(); await waitForPromises();
@ -140,12 +141,12 @@ describe('Sidebar Confidentiality Widget', () => {
expect(findConfidentialityForm().isVisible()).toBe(true); expect(findConfidentialityForm().isVisible()).toBe(true);
findConfidentialityForm().vm.$emit('closeForm'); findConfidentialityForm().vm.$emit('closeForm', closeFormPayload);
await nextTick(); await nextTick();
expect(findConfidentialityForm().isVisible()).toBe(false); expect(findConfidentialityForm().isVisible()).toBe(false);
expect(el.dispatchEvent).toHaveBeenCalled(); expect(el.dispatchEvent).toHaveBeenCalled();
expect(wrapper.emitted('closeForm')).toHaveLength(1); expect(wrapper.emitted('closeForm')).toEqual([[closeFormPayload]]);
}); });
it('emits `expandSidebar` event when it is emitted from child component', async () => { it('emits `expandSidebar` event when it is emitted from child component', async () => {

View File

@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import SidebarEventHub from '~/sidebar/event_hub';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@ -162,6 +163,21 @@ describe('WorkItemLinks', () => {
expect(findChildrenCount().text()).toContain('4'); expect(findChildrenCount().text()).toContain('4');
}); });
it('refetches child items when `confidentialityUpdated` event is emitted on SidebarEventhub', async () => {
const fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse);
await createComponent({
fetchHandler,
});
await waitForPromises();
SidebarEventHub.$emit('confidentialityUpdated');
await nextTick();
// First call is done on component mount.
// Second call is done on confidentialityUpdated event.
expect(fetchHandler).toHaveBeenCalledTimes(2);
});
describe('when no permission to update', () => { describe('when no permission to update', () => {
beforeEach(async () => { beforeEach(async () => {
await createComponent({ await createComponent({

View File

@ -64,7 +64,6 @@ RSpec.describe Ci::PipelineEditorHelper do
"project-path" => project.path, "project-path" => project.path,
"project-full-path" => project.full_path, "project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path, "project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/index'),
"simulate-pipeline-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'simulate-a-cicd-pipeline'), "simulate-pipeline-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'simulate-a-cicd-pipeline'),
"total-branches" => project.repository.branches.length, "total-branches" => project.repository.branches.length,
"validate-tab-illustration-path" => 'illustrations/validate.svg', "validate-tab-illustration-path" => 'illustrations/validate.svg',
@ -95,7 +94,6 @@ RSpec.describe Ci::PipelineEditorHelper do
"project-path" => project.path, "project-path" => project.path,
"project-full-path" => project.full_path, "project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path, "project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/index'),
"simulate-pipeline-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'simulate-a-cicd-pipeline'), "simulate-pipeline-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'simulate-a-cicd-pipeline'),
"total-branches" => 0, "total-branches" => 0,
"validate-tab-illustration-path" => 'illustrations/validate.svg', "validate-tab-illustration-path" => 'illustrations/validate.svg',

View File

@ -5,6 +5,24 @@ require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
let(:connection) { Gitlab::Database.database_base_models[:main].connection } let(:connection) { Gitlab::Database.database_base_models[:main].connection }
describe '.generic_instance' do
it 'defines generic instance with only some of the attributes set' do
generic_instance = described_class.generic_instance(
batch_table: 'projects', batch_column: 'id',
job_arguments: %w(x y), connection: connection
)
expect(generic_instance.send(:batch_table)).to eq('projects')
expect(generic_instance.send(:batch_column)).to eq('id')
expect(generic_instance.instance_variable_get('@job_arguments')).to eq(%w(x y))
expect(generic_instance.send(:connection)).to eq(connection)
%i(start_id end_id sub_batch_size pause_ms).each do |attr|
expect(generic_instance.send(attr)).to eq(0)
end
end
end
describe '.job_arguments' do describe '.job_arguments' do
let(:job_class) do let(:job_class) do
Class.new(described_class) do Class.new(described_class) do
@ -39,6 +57,59 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
end end
end end
describe '.scope_to' do
subject(:job_instance) do
job_class.new(start_id: 1, end_id: 10,
batch_table: '_test_table',
batch_column: 'id',
sub_batch_size: 2,
pause_ms: 1000,
job_arguments: %w(a b),
connection: connection)
end
context 'when additional scoping is defined' do
let(:job_class) do
Class.new(described_class) do
job_arguments :value_a, :value_b
scope_to ->(r) { "#{r}-#{value_a}-#{value_b}".upcase }
end
end
it 'applies additional scope to the provided relation' do
expect(job_instance.filter_batch('relation')).to eq('RELATION-A-B')
end
end
context 'when there is no additional scoping defined' do
let(:job_class) do
Class.new(described_class) do
end
end
it 'returns provided relation as is' do
expect(job_instance.filter_batch('relation')).to eq('relation')
end
end
end
describe 'descendants', :eager_load do
it 'have the same method signature for #perform' do
expected_arity = described_class.instance_method(:perform).arity
offences = described_class.descendants.select { |klass| klass.instance_method(:perform).arity != expected_arity }
expect(offences).to be_empty, "expected no descendants of #{described_class} to accept arguments for " \
"'#perform', but some do: #{offences.join(", ")}"
end
it 'do not use .batching_scope' do
offences = described_class.descendants.select { |klass| klass.respond_to?(:batching_scope) }
expect(offences).to be_empty, "expected no descendants of #{described_class} to define '.batching_scope', " \
"but some do: #{offences.join(", ")}"
end
end
describe '#perform' do describe '#perform' do
let(:connection) { Gitlab::Database.database_base_models[:main].connection } let(:connection) { Gitlab::Database.database_base_models[:main].connection }
@ -59,14 +130,6 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
expect { perform_job }.to raise_error(NotImplementedError, /must implement perform/) expect { perform_job }.to raise_error(NotImplementedError, /must implement perform/)
end end
it 'expects descendants to have the same method signature', :eager_load do
expected_arity = described_class.instance_method(:perform).arity
offences = described_class.descendants.select { |klass| klass.instance_method(:perform).arity != expected_arity }
expect(offences).to be_empty, "expected no descendants of #{described_class} to accept arguments for #perform, " \
"but some do: #{offences.join(", ")}"
end
context 'when the subclass uses sub-batching' do context 'when the subclass uses sub-batching' do
let(:job_class) do let(:job_class) do
Class.new(described_class) do Class.new(described_class) do
@ -110,6 +173,30 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
expect(test_table.order(:id).pluck(:to_column)).to contain_exactly(5, 10, nil, 20) expect(test_table.order(:id).pluck(:to_column)).to contain_exactly(5, 10, nil, 20)
end end
context 'with additional scoping' do
let(:job_class) do
Class.new(described_class) do
scope_to ->(r) { r.where('mod(id, 2) = 0') }
def perform(*job_arguments)
each_sub_batch(
operation_name: :update,
batching_arguments: { order_hint: :updated_at },
batching_scope: -> (relation) { relation.where.not(bar: nil) }
) do |sub_batch|
sub_batch.update_all('to_column = from_column')
end
end
end
end
it 'respects #filter_batch' do
expect { perform_job }.to change { test_table.where(to_column: nil).count }.from(4).to(2)
expect(test_table.order(:id).pluck(:to_column)).to contain_exactly(nil, 10, nil, 20)
end
end
it 'instruments the batch operation' do it 'instruments the batch operation' do
expect(job_instance.batch_metrics.affected_rows).to be_empty expect(job_instance.batch_metrics.affected_rows).to be_empty
@ -128,7 +215,7 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
context 'when batching_arguments are given' do context 'when batching_arguments are given' do
it 'forwards them for batching' do it 'forwards them for batching' do
expect(job_instance).to receive(:parent_batch_relation).and_return(test_table) expect(job_instance).to receive(:base_relation).and_return(test_table)
expect(test_table).to receive(:each_batch).with(column: 'id', of: 2, order_hint: :updated_at) expect(test_table).to receive(:each_batch).with(column: 'id', of: 2, order_hint: :updated_at)
@ -199,6 +286,24 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
expect(job_instance.batch_metrics.affected_rows[:insert]).to contain_exactly(2, 1) expect(job_instance.batch_metrics.affected_rows[:insert]).to contain_exactly(2, 1)
end end
context 'when used in combination with scope_to' do
let(:job_class) do
Class.new(described_class) do
scope_to ->(r) { r.where.not(from_column: 10) }
def perform(*job_arguments)
distinct_each_batch(operation_name: :insert) do |sub_batch|
end
end
end
end
it 'raises an error' do
expect { perform_job }.to raise_error RuntimeError,
/distinct_each_batch can not be used when additional filters are defined with scope_to/
end
end
end end
end end
end end

View File

@ -45,19 +45,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
end end
end end
context 'when job_class is provided with a batching_scope' do context 'when job class supports batch scope DSL' do
let(:job_class) do let(:job_class) do
Class.new(described_class) do Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
def self.batching_scope(relation, job_arguments:) job_arguments :min_id
min_id = job_arguments.first scope_to ->(r) { r.where.not(type: 'Project').where('id >= ?', min_id) }
relation.where.not(type: 'Project').where('id >= ?', min_id)
end
end end
end end
it 'applies the batching scope' do it 'applies the additional scope' do
expect(job_class).to receive(:batching_scope).and_call_original expect(job_class).to receive(:generic_instance).and_call_original
batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [1], job_class: job_class) batch_bounds = batching_strategy.next_batch(:namespaces, :id, batch_min_value: namespace4.id, batch_size: 3, job_arguments: [1], job_class: job_class)

View File

@ -3232,6 +3232,62 @@ RSpec.describe MergeRequest, factory_default: :keep do
end end
end end
describe '#detailed_merge_status' do
subject(:detailed_merge_status) { merge_request.detailed_merge_status }
context 'when merge status is cannot_be_merged_rechecking' do
let(:merge_request) { create(:merge_request, merge_status: :cannot_be_merged_rechecking) }
it 'returns :checking' do
expect(detailed_merge_status).to eq(:checking)
end
end
context 'when merge status is preparing' do
let(:merge_request) { create(:merge_request, merge_status: :preparing) }
it 'returns :checking' do
expect(detailed_merge_status).to eq(:checking)
end
end
context 'when merge status is checking' do
let(:merge_request) { create(:merge_request, merge_status: :checking) }
it 'returns :checking' do
expect(detailed_merge_status).to eq(:checking)
end
end
context 'when merge status is unchecked' do
let(:merge_request) { create(:merge_request, merge_status: :unchecked) }
it 'returns :unchecked' do
expect(detailed_merge_status).to eq(:unchecked)
end
end
context 'when merge checks are a success' do
let(:merge_request) { create(:merge_request) }
it 'returns :mergeable' do
expect(detailed_merge_status).to eq(:mergeable)
end
end
context 'when merge status have a failure' do
let(:merge_request) { create(:merge_request) }
before do
merge_request.close!
end
it 'returns the failure reason' do
expect(detailed_merge_status).to eq(:not_open)
end
end
end
describe '#mergeable_state?' do describe '#mergeable_state?' do
it_behaves_like 'for mergeable_state' it_behaves_like 'for mergeable_state'

View File

@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:merge_request) { build(:merge_request) } let(:merge_request) { build(:merge_request) }
describe '#execute' do describe '#execute' do
let(:result) { check_broken_status.execute }
before do before do
expect(merge_request).to receive(:broken?).and_return(broken) expect(merge_request).to receive(:broken?).and_return(broken)
end end
@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:broken) { true } let(:broken) { true }
it 'returns a check result with status failed' do it 'returns a check result with status failed' do
expect(check_broken_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
expect(result.payload[:reason]).to eq(:broken_status)
end end
end end
@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:broken) { false } let(:broken) { false }
it 'returns a check result with status success' do it 'returns a check result with status success' do
expect(check_broken_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end end
end end
end end

View File

@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:skip_check) { false } let(:skip_check) { false }
describe '#execute' do describe '#execute' do
let(:result) { check_ci_status.execute }
before do before do
expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable) expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable)
end end
@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:mergeable) { true } let(:mergeable) { true }
it 'returns a check result with status success' do it 'returns a check result with status success' do
expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end end
end end
@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:mergeable) { false } let(:mergeable) { false }
it 'returns a check result with status failed' do it 'returns a check result with status failed' do
expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
expect(result.payload[:reason]).to eq :ci_must_pass
end end
end end
end end

View File

@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:skip_check) { false } let(:skip_check) { false }
describe '#execute' do describe '#execute' do
let(:result) { check_discussions_status.execute }
before do before do
expect(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable) expect(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable)
end end
@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:mergeable) { true } let(:mergeable) { true }
it 'returns a check result with status success' do it 'returns a check result with status success' do
expect(check_discussions_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end end
end end
@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:mergeable) { false } let(:mergeable) { false }
it 'returns a check result with status failed' do it 'returns a check result with status failed' do
expect(check_discussions_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
expect(result.payload[:reason]).to eq(:discussions_not_resolved)
end end
end end
end end

View File

@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:merge_request) { build(:merge_request) } let(:merge_request) { build(:merge_request) }
describe '#execute' do describe '#execute' do
let(:result) { check_draft_status.execute }
before do before do
expect(merge_request).to receive(:draft?).and_return(draft) expect(merge_request).to receive(:draft?).and_return(draft)
end end
@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:draft) { true } let(:draft) { true }
it 'returns a check result with status failed' do it 'returns a check result with status failed' do
expect(check_draft_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
expect(result.payload[:reason]).to eq(:draft_status)
end end
end end
@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:draft) { false } let(:draft) { false }
it 'returns a check result with status success' do it 'returns a check result with status success' do
expect(check_draft_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end end
end end
end end

View File

@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:merge_request) { build(:merge_request) } let(:merge_request) { build(:merge_request) }
describe '#execute' do describe '#execute' do
let(:result) { check_open_status.execute }
before do before do
expect(merge_request).to receive(:open?).and_return(open) expect(merge_request).to receive(:open?).and_return(open)
end end
@ -16,7 +18,7 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:open) { true } let(:open) { true }
it 'returns a check result with status success' do it 'returns a check result with status success' do
expect(check_open_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS
end end
end end
@ -24,7 +26,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:open) { false } let(:open) { false }
it 'returns a check result with status failed' do it 'returns a check result with status failed' do
expect(check_open_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS
expect(result.payload[:reason]).to eq(:not_open)
end end
end end
end end

View File

@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::RunChecksService do RSpec.describe MergeRequests::Mergeability::RunChecksService do
subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) } subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) }
let_it_be(:merge_request) { create(:merge_request) }
describe '#execute' do describe '#execute' do
subject(:execute) { run_checks.execute } subject(:execute) { run_checks.execute }
let_it_be(:merge_request) { create(:merge_request) }
let(:params) { {} } let(:params) { {} }
let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success } let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success }
@ -23,7 +23,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
end end
it 'is still a success' do it 'is still a success' do
expect(execute.all?(&:success?)).to eq(true) expect(execute.success?).to eq(true)
end end
end end
@ -41,13 +41,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).not_to receive(:execute) expect(service).not_to receive(:execute)
end end
# Since we're only marking one check to be skipped, we expect to receive expect(execute.success?).to eq(true)
# `# of checks - 1` success result objects in return
#
check_count = merge_request.mergeability_checks.count - 1
success_array = (1..check_count).each_with_object([]) { |_, array| array << success_result }
expect(execute).to match_array(success_array)
end end
end end
@ -75,7 +69,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result) expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result)
end end
expect(execute).to match_array([success_result]) expect(execute.success?).to eq(true)
end end
end end
@ -86,7 +80,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true) expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true)
end end
expect(execute).to match_array([success_result]) expect(execute.success?).to eq(true)
end end
end end
end end
@ -97,7 +91,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
it 'does not call the results store' do it 'does not call the results store' do
expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new)
expect(execute).to match_array([success_result]) expect(execute.success?).to eq(true)
end end
end end
@ -109,9 +103,81 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
it 'does not call the results store' do it 'does not call the results store' do
expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new)
expect(execute).to match_array([success_result]) expect(execute.success?).to eq(true)
end end
end end
end end
end end
describe '#success?' do
subject(:success) { run_checks.success? }
let_it_be(:merge_request) { create(:merge_request) }
context 'when the execute method has been executed' do
before do
run_checks.execute
end
context 'when all the checks succeed' do
it 'returns true' do
expect(success).to eq(true)
end
end
context 'when one check fails' do
before do
allow(merge_request).to receive(:open?).and_return(false)
run_checks.execute
end
it 'returns false' do
expect(success).to eq(false)
end
end
end
context 'when execute has not been exectued' do
it 'raises an error' do
expect { subject }
.to raise_error(/Execute needs to be called before/)
end
end
end
describe '#failure_reason' do
subject(:failure_reason) { run_checks.failure_reason }
let_it_be(:merge_request) { create(:merge_request) }
context 'when the execute method has been executed' do
before do
run_checks.execute
end
context 'when all the checks succeed' do
it 'returns nil' do
expect(failure_reason).to eq(nil)
end
end
context 'when one check fails' do
before do
allow(merge_request).to receive(:open?).and_return(false)
run_checks.execute
end
it 'returns the open reason' do
expect(failure_reason).to eq(:not_open)
end
end
end
context 'when execute has not been exectued' do
it 'raises an error' do
expect { subject }
.to raise_error(/Execute needs to be called before/)
end
end
end
end end