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:
Enabled: false
RSpec/Be:
Enabled: false
RSpec/DescribedClass:
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 NEW_ROUTE_NAME = 'new';
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 VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CrmContactsRoot from './components/contacts_root.vue';
import routes from './routes';
@ -21,7 +22,14 @@ export default () => {
return false;
}
const { basePath, groupFullPath, groupIssuesPath, canAdminCrmContact, groupId } = el.dataset;
const {
basePath,
groupFullPath,
groupIssuesPath,
canAdminCrmContact,
groupId,
textQuery,
} = el.dataset;
const router = new VueRouter({
base: basePath,
@ -33,7 +41,13 @@ export default () => {
el,
router,
apolloProvider,
provide: { groupFullPath, groupIssuesPath, canAdminCrmContact, groupId },
provide: {
groupFullPath,
groupIssuesPath,
canAdminCrmContact: parseBoolean(canAdminCrmContact),
groupId,
textQuery,
},
render(createElement) {
return createElement(CrmContactsRoot);
},

View File

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

View File

@ -1,36 +1,54 @@
<script>
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { EDIT_ROUTE_NAME, NEW_ROUTE_NAME } from '../../constants';
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
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 {
components: {
GlAlert,
GlButton,
GlLoadingIcon,
GlTable,
PaginatedTableWithSearchAndTabs,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath'],
inject: ['canAdminCrmContact', 'groupFullPath', 'groupIssuesPath', 'textQuery'],
data() {
return {
contacts: [],
contacts: { list: [] },
contactsCount: {},
error: false,
filteredByStatus: '',
pagination: initialPaginationState,
statusFilter: 'all',
searchTerm: this.textQuery,
sort: 'LAST_NAME_ASC',
sortDesc: false,
};
},
apollo: {
contacts: {
query() {
return getGroupContactsQuery;
},
query: getGroupContacts,
variables() {
return {
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) {
@ -40,19 +58,52 @@ export default {
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: {
isLoading() {
return this.$apollo.queries.contacts.loading;
},
canAdmin() {
return parseBoolean(this.canAdminCrmContact);
tbodyTrClass() {
return {
[bodyTrClass]: !this.loading && !this.isEmpty,
};
},
},
methods: {
errorAlertDismissed() {
this.error = true;
},
extractContacts(data) {
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) {
return `${path}?crm_contact_id=${value}`;
@ -60,6 +111,13 @@ export default {
getEditRoute(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: [
{ key: 'firstName', sortable: true },
@ -92,57 +150,109 @@ export default {
},
EDIT_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>
<template>
<div>
<gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = false">
{{ $options.i18n.errorText }}
</gl-alert>
<div
class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6"
<paginated-table-with-search-and-tabs
:show-items="true"
:show-error-msg="false"
:i18n="$options.i18n"
: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">
{{ $options.i18n.title }}
</h2>
<div v-if="canAdmin">
<router-link :to="{ name: $options.NEW_ROUTE_NAME }">
<gl-button variant="confirm" data-testid="new-contact-button">
<template #header-actions>
<router-link v-if="canAdminCrmContact" :to="{ name: $options.NEW_ROUTE_NAME }">
<gl-button class="gl-my-3 gl-mr-5" variant="confirm" data-testid="new-contact-button">
{{ $options.i18n.newContact }}
</gl-button>
</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>
</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>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -95,10 +95,10 @@ export default {
confidentialWidget.setConfidentiality = null;
},
methods: {
closeForm() {
closeForm({ confidential } = {}) {
this.$refs.editable.collapse();
this.$el.dispatchEvent(hideDropdownEvent);
this.$emit('closeForm');
this.$emit('closeForm', { confidential });
},
// synchronizing the quick action with the sidebar widget
// 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 SidebarMoveIssue from './lib/sidebar_move_issue';
import CrmContacts from './components/crm_contacts/crm_contacts.vue';
import SidebarEventHub from './event_hub';
Vue.use(Translate);
Vue.use(VueApollo);
@ -359,6 +360,13 @@ function mountConfidentialComponent() {
? IssuableType.Issue
: 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 { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import SidebarEventHub from '~/sidebar/event_hub';
import {
STATE_OPEN,
WIDGET_ICONS,
@ -111,7 +113,16 @@ export default {
return this.isLoading && this.children.length === 0 ? '...' : this.children.length;
},
},
mounted() {
SidebarEventHub.$on('confidentialityUpdated', this.refetchWorkItems);
},
destroyed() {
SidebarEventHub.$off('confidentialityUpdated', this.refetchWorkItems);
},
methods: {
refetchWorkItems() {
this.$apollo.queries.workItem.refetch();
},
badgeVariant(state) {
return state === STATE_OPEN ? 'success' : 'info';
},
@ -122,6 +133,7 @@ export default {
this.isOpen = !this.isOpen;
},
showAddForm() {
this.isOpen = true;
this.isShownAddForm = true;
this.$nextTick(() => {
this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus();

View File

@ -93,6 +93,11 @@ module Types
field :merge_status_enum, ::Types::MergeRequests::MergeStatusEnum,
method: :public_merge_status, null: true,
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,
calls_gitaly: true,
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-full-path" => project.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'),
"total-branches" => total_branches,
"validate-tab-illustration-path" => image_path('illustrations/project-run-CICD-pipelines-sm.svg'),

View File

@ -1187,17 +1187,30 @@ class MergeRequest < ApplicationRecord
]
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
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
if Feature.enabled?(:improved_mergeability_checks, self.project)
additional_checks = MergeRequests::Mergeability::RunChecksService.new(
merge_request: self,
params: {
skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check
}
)
additional_checks.execute.all?(&:success?)
additional_checks = execute_merge_checks(params: {
skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check
})
additional_checks.execute.success?
else
return false unless open?
return false if draft?
@ -2059,6 +2072,12 @@ class MergeRequest < ApplicationRecord
def report_type_enabled?(report_type)
!!actual_head_pipeline&.batch_lookup_report_artifact_for_file_type(report_type)
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
MergeRequest.prepend_mod_with('MergeRequest')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,36 +10,50 @@ module MergeRequests
end
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)
next if check.skip?
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
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
private
attr_reader :merge_request, :params
attr_reader :merge_request, :params, :results
def run_check(check)
return check.execute unless Feature.enabled?(:mergeability_caching, merge_request.project)
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)
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
def results
strong_memoize(:results) do
def cached_results
strong_memoize(:cached_results) do
Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request)
end
end

View File

@ -22,7 +22,7 @@
.form-group
= 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
.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
= f.label :max_artifacts_size, _('Maximum artifacts size (MB)'), class: 'label-bold'
= f.number_field :max_artifacts_size, class: 'form-control gl-form-input'

View File

@ -5,4 +5,4 @@
= content_for :after_content do
#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="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="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="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. |
@ -19580,6 +19581,24 @@ Mutation event of a design within a version.
| <a id="designversioneventmodification"></a>`MODIFICATION` | A modification event. |
| <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`
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`.
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
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
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
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
```
### 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
The `routes` table has a `source_type` field that's used for a polymorphic relationship.
@ -419,76 +490,8 @@ module Gitlab
end
```
### Adding filters to the initial batching
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).
NOTE:
[Additional filters](#additional-filters) defined with `scope_to` will be ignored by `LooseIndexScanBatchingStrategy` and `distinct_each_batch`.
## 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
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.
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:
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. 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. 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
model of how they each behave.
- 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
logic and structure differences.
- 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
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

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.
```mermaid
graph LR
graph TB
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
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.
```mermaid
flowchart LR
flowchart TB
subgraph Default branch
A[Commit] --> B[Commit] --> C[Commit] --> D[Commit]
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).
- [**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>

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/).
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.

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/).
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.

View File

@ -47,6 +47,28 @@ graph TD
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
Prerequisites:

View File

@ -16,9 +16,26 @@ to automatically manage your container registry usage.
## Check Container Registry Storage Use
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
[Storage management for the Container Registry](https://gitlab.com/groups/gitlab-org/-/epics/7226).
The Usage Quotas page (**Settings > Usage Quotas > Storage**) displays storage usage for Packages.
This page includes the Container Registry usage but is currently only available on GitLab.com.
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

View File

@ -157,6 +157,9 @@ Supported values are:
| `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) |
NOTE:
The `dora` data source only supports the `bar` chart type.
### `query`
`query` allows to define the data source and various filtering conditions for the chart.
@ -349,7 +352,7 @@ dora:
title: "DORA charts"
charts:
- title: "DORA deployment frequency"
type: bar
type: bar # only bar chart is supported at the moment
query:
data_source: dora
params:
@ -372,7 +375,7 @@ dora:
period_limit: 30
```
#### `query.metric`
##### `query.metric`
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`
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`

View File

@ -150,7 +150,8 @@ considered equivalent to rebasing.
### 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
**Rebase without pipeline** from the merge request reports section.

View File

@ -24,6 +24,14 @@ module Gitlab
@connection = connection
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
0
end
@ -40,6 +48,16 @@ module Gitlab
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
raise NotImplementedError, "subclasses of #{self.class.name} must implement #{__method__}"
end
@ -55,9 +73,10 @@ module Gitlab
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)
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
yield relation
end
@ -67,9 +86,13 @@ module Gitlab
end
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)
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
yield relation
end
@ -78,13 +101,15 @@ module Gitlab
end
end
def parent_batch_relation(batching_scope = nil)
parent_relation = define_batchable_model(batch_table, connection: connection)
def base_relation
define_batchable_model(batch_table, connection: connection)
.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

View File

@ -24,6 +24,14 @@ module Gitlab
quoted_column_name = model_class.connection.quote_column_name(column_name)
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)
next_batch_bounds = nil
@ -36,13 +44,27 @@ module Gitlab
next_batch_bounds
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)
if job_class.respond_to?(:batching_scope)
return job_class.batching_scope(relation, job_arguments: job_arguments)
end
relation
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

View File

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

View File

@ -2145,7 +2145,7 @@ msgstr ""
msgid "Add a confidential internal note to this %{noteableDisplayName}."
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 ""
msgid "Add a general comment to this %{noteableDisplayName}."
@ -16799,8 +16799,8 @@ msgstr ""
msgid "From %{providerTitle}"
msgstr ""
msgid "From October 19, 2022, free groups will be limited to %d member"
msgid_plural "From October 19, 2022, free groups will be limited to %d members"
msgid "From October 19, 2022, free private groups will be limited to %d member"
msgid_plural "From October 19, 2022, free private groups will be limited to %d members"
msgstr[0] ""
msgstr[1] ""

View File

@ -1,14 +1,16 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
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 waitForPromises from 'helpers/wait_for_promises';
import ContactsRoot from '~/crm/contacts/components/contacts_root.vue';
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 { 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', () => {
Vue.use(VueApollo);
@ -21,24 +23,30 @@ describe('Customer relations contacts root app', () => {
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
const findNewContactButton = () => wrapper.findByTestId('new-contact-button');
const findError = () => wrapper.findComponent(GlAlert);
const findTable = () => wrapper.findComponent(PaginatedTableWithSearchAndTabs);
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
const successCountQueryHandler = jest.fn().mockResolvedValue(getGroupContactsCountQueryResponse);
const basePath = '/groups/flightjs/-/crm/contacts';
const mountComponent = ({
queryHandler = successQueryHandler,
mountFunction = shallowMountExtended,
countQueryHandler = successCountQueryHandler,
canAdminCrmContact = true,
textQuery = null,
} = {}) => {
fakeApollo = createMockApollo([[getGroupContactsQuery, queryHandler]]);
wrapper = mountFunction(ContactsRoot, {
fakeApollo = createMockApollo([
[getGroupContactsQuery, queryHandler],
[getGroupContactsCountByStateQuery, countQueryHandler],
]);
wrapper = mountExtended(ContactsRoot, {
router,
provide: {
groupFullPath: 'flightjs',
groupId: 26,
groupIssuesPath: '/issues',
canAdminCrmContact,
textQuery,
},
apolloProvider: fakeApollo,
});
@ -58,9 +66,33 @@ describe('Customer relations contacts root app', () => {
router = null;
});
it('should render loading spinner', () => {
it('should render table with default props and loading state', () => {
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);
});
@ -83,7 +115,7 @@ describe('Customer relations contacts root app', () => {
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
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();
await waitForPromises();
expect(findError().exists()).toBe(false);
expect(wrapper.text()).not.toContain('Something went wrong. Please try again.');
});
it('renders correct results', async () => {
mountComponent({ mountFunction: mountExtended });
mountComponent();
await waitForPromises();
expect(findRowByName(/Marty/i)).toHaveLength(1);
@ -105,7 +137,7 @@ describe('Customer relations contacts root app', () => {
const issueLink = findIssuesLinks().at(0);
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,
},
],
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 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);

View File

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

View File

@ -71,7 +71,12 @@ describe('Sidebar Confidentiality Form', () => {
it('creates a flash if mutation contains errors', async () => {
createComponent({
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'));
@ -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', () => {
beforeEach(() => {
createComponent();

View File

@ -132,6 +132,7 @@ describe('Sidebar Confidentiality Widget', () => {
it('closes the form and dispatches an event when `closeForm` is emitted', async () => {
createComponent();
const el = wrapper.vm.$el;
const closeFormPayload = { confidential: true };
jest.spyOn(el, 'dispatchEvent');
await waitForPromises();
@ -140,12 +141,12 @@ describe('Sidebar Confidentiality Widget', () => {
expect(findConfidentialityForm().isVisible()).toBe(true);
findConfidentialityForm().vm.$emit('closeForm');
findConfidentialityForm().vm.$emit('closeForm', closeFormPayload);
await nextTick();
expect(findConfidentialityForm().isVisible()).toBe(false);
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 () => {

View File

@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SidebarEventHub from '~/sidebar/event_hub';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@ -162,6 +163,21 @@ describe('WorkItemLinks', () => {
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', () => {
beforeEach(async () => {
await createComponent({

View File

@ -64,7 +64,6 @@ RSpec.describe Ci::PipelineEditorHelper do
"project-path" => project.path,
"project-full-path" => project.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'),
"total-branches" => project.repository.branches.length,
"validate-tab-illustration-path" => 'illustrations/validate.svg',
@ -95,7 +94,6 @@ RSpec.describe Ci::PipelineEditorHelper do
"project-path" => project.path,
"project-full-path" => project.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'),
"total-branches" => 0,
"validate-tab-illustration-path" => 'illustrations/validate.svg',

View File

@ -5,6 +5,24 @@ require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
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
let(:job_class) do
Class.new(described_class) do
@ -39,6 +57,59 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
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
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/)
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
let(:job_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)
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
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
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)
@ -199,6 +286,24 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
expect(job_instance.batch_metrics.affected_rows[:insert]).to contain_exactly(2, 1)
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

View File

@ -45,19 +45,16 @@ RSpec.describe Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchi
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
Class.new(described_class) do
def self.batching_scope(relation, job_arguments:)
min_id = job_arguments.first
relation.where.not(type: 'Project').where('id >= ?', min_id)
end
Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
job_arguments :min_id
scope_to ->(r) { r.where.not(type: 'Project').where('id >= ?', min_id) }
end
end
it 'applies the batching scope' do
expect(job_class).to receive(:batching_scope).and_call_original
it 'applies the additional scope' do
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)

View File

@ -3232,6 +3232,62 @@ RSpec.describe MergeRequest, factory_default: :keep do
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
it_behaves_like 'for mergeable_state'

View File

@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:merge_request) { build(:merge_request) }
describe '#execute' do
let(:result) { check_broken_status.execute }
before do
expect(merge_request).to receive(:broken?).and_return(broken)
end
@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:broken) { true }
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
@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do
let(:broken) { false }
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

View File

@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:skip_check) { false }
describe '#execute' do
let(:result) { check_ci_status.execute }
before do
expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable)
end
@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:mergeable) { true }
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
@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do
let(:mergeable) { false }
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

View File

@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:skip_check) { false }
describe '#execute' do
let(:result) { check_discussions_status.execute }
before do
expect(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable)
end
@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:mergeable) { true }
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
@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do
let(:mergeable) { false }
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

View File

@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:merge_request) { build(:merge_request) }
describe '#execute' do
let(:result) { check_draft_status.execute }
before do
expect(merge_request).to receive(:draft?).and_return(draft)
end
@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:draft) { true }
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
@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do
let(:draft) { false }
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

View File

@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:merge_request) { build(:merge_request) }
describe '#execute' do
let(:result) { check_open_status.execute }
before do
expect(merge_request).to receive(:open?).and_return(open)
end
@ -16,7 +18,7 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:open) { true }
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
@ -24,7 +26,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do
let(:open) { false }
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

View File

@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe MergeRequests::Mergeability::RunChecksService do
subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) }
let_it_be(:merge_request) { create(:merge_request) }
describe '#execute' do
subject(:execute) { run_checks.execute }
let_it_be(:merge_request) { create(:merge_request) }
let(:params) { {} }
let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success }
@ -23,7 +23,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
end
it 'is still a success' do
expect(execute.all?(&:success?)).to eq(true)
expect(execute.success?).to eq(true)
end
end
@ -41,13 +41,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).not_to receive(:execute)
end
# Since we're only marking one check to be skipped, we expect to receive
# `# 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)
expect(execute.success?).to eq(true)
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)
end
expect(execute).to match_array([success_result])
expect(execute.success?).to eq(true)
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)
end
expect(execute).to match_array([success_result])
expect(execute.success?).to eq(true)
end
end
end
@ -97,7 +91,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
it 'does not call the results store' do
expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new)
expect(execute).to match_array([success_result])
expect(execute.success?).to eq(true)
end
end
@ -109,9 +103,81 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
it 'does not call the results store' do
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
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