Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-01-31 12:12:04 +00:00
parent 584f7c6eba
commit e033caddff
76 changed files with 821 additions and 268 deletions

View File

@ -1,6 +1,21 @@
include:
- local: .gitlab/ci/rails/shared.gitlab-ci.yml
db:setup pg14:
stage: prepare
needs: []
extends:
- .use-pg14
- .default-before_script
- .ruby-cache
- .rails:rules:setup-test-env
script:
- source scripts/utils.sh
- run_timed_command "pg_dumpall -h postgres -U postgres > pg_dumpall.sql"
artifacts:
paths:
- pg_dumpall.sql
db:rollback single-db-ci-connection:
extends:
- db:rollback

View File

@ -210,6 +210,10 @@ include:
- !reference [.rspec-base, after_script]
.rspec-base-pg14:
needs:
- !reference [.rspec-base, needs]
- job: "db:setup pg14"
optional: true
extends:
- .rspec-base
- .use-pg14

View File

@ -1,22 +0,0 @@
---
# Cop supports --autocorrect.
RSpec/BeEmpty:
Exclude:
- 'ee/spec/models/concerns/elastic/application_versioned_search_spec.rb'
- 'ee/spec/models/ee/group_spec.rb'
- 'ee/spec/models/ee/user_spec.rb'
- 'ee/spec/models/product_analytics/visualization_spec.rb'
- 'ee/spec/requests/api/graphql/ai/feature_settings/feature_settings_spec.rb'
- 'ee/spec/services/releases/update_service_spec.rb'
- 'ee/spec/support/shared_examples/lib/sidebars/menus_shared_examples.rb'
- 'ee/spec/support/shared_examples/models/authz/member_roles_shared_examples.rb'
- 'ee/spec/support/shared_examples/quick_actions/merge_request/unassign_reviewer_shared_examples.rb'
- 'spec/helpers/nav/new_dropdown_helper_spec.rb'
- 'spec/helpers/users_helper_spec.rb'
- 'spec/lib/click_house/iterator_spec.rb'
- 'spec/lib/gitlab/checks/changes_access_spec.rb'
- 'spec/lib/gitlab/ci/jwt_spec.rb'
- 'spec/lib/gitlab/ci/variables/builder/release_spec.rb'
- 'spec/lib/gitlab/mail_room/mail_room_spec.rb'
- 'spec/lib/gitlab/search_results_spec.rb'
- 'spec/lib/search/empty_search_results_spec.rb'

View File

@ -709,7 +709,7 @@
{"name":"sprockets-rails","version":"3.5.1","platform":"ruby","checksum":"c44626cb3887a1a8b572ca258685db33b4ebd041aa73428a716eac444ee5ef48"},
{"name":"ssh_data","version":"1.3.0","platform":"ruby","checksum":"ec7c1e95a3aebeee412147998f4c147b4b05da6ed0aafda6083f9449318eaac0"},
{"name":"ssrf_filter","version":"1.0.8","platform":"ruby","checksum":"03f49f54837e407d43ee93ec733a8a94dc1bcf8185647ac61606e63aaedaa0db"},
{"name":"stackprof","version":"0.2.26","platform":"ruby","checksum":"ee408cbcccd9422aabd66edff8b76a77d67955f2ee1b674961b5dfaa2cc7b8bd"},
{"name":"stackprof","version":"0.2.27","platform":"ruby","checksum":"aff6d28656c852e74cf632cc2046f849033dc1dedffe7cb8c030d61b5745e80c"},
{"name":"state_machines","version":"0.5.0","platform":"ruby","checksum":"23e6249d374a920b528dccade403518b4abbd83841a3e2c9ef13e6f1a009b102"},
{"name":"state_machines-activemodel","version":"0.8.0","platform":"ruby","checksum":"e932dab190d4be044fb5f9cab01a3ea0b092c5f113d4676c6c0a0d49bf738d2c"},
{"name":"state_machines-activerecord","version":"0.8.0","platform":"ruby","checksum":"072fb701b8ab03de0608297f6c55dc34ed096e556fa8f77e556f3c461c71aab6"},

View File

@ -1803,7 +1803,7 @@ GEM
sprockets (>= 3.0.0)
ssh_data (1.3.0)
ssrf_filter (1.0.8)
stackprof (0.2.26)
stackprof (0.2.27)
state_machines (0.5.0)
state_machines-activemodel (0.8.0)
activemodel (>= 5.1)

View File

@ -720,7 +720,7 @@
{"name":"sprockets-rails","version":"3.5.1","platform":"ruby","checksum":"c44626cb3887a1a8b572ca258685db33b4ebd041aa73428a716eac444ee5ef48"},
{"name":"ssh_data","version":"1.3.0","platform":"ruby","checksum":"ec7c1e95a3aebeee412147998f4c147b4b05da6ed0aafda6083f9449318eaac0"},
{"name":"ssrf_filter","version":"1.0.8","platform":"ruby","checksum":"03f49f54837e407d43ee93ec733a8a94dc1bcf8185647ac61606e63aaedaa0db"},
{"name":"stackprof","version":"0.2.26","platform":"ruby","checksum":"ee408cbcccd9422aabd66edff8b76a77d67955f2ee1b674961b5dfaa2cc7b8bd"},
{"name":"stackprof","version":"0.2.27","platform":"ruby","checksum":"aff6d28656c852e74cf632cc2046f849033dc1dedffe7cb8c030d61b5745e80c"},
{"name":"state_machines","version":"0.5.0","platform":"ruby","checksum":"23e6249d374a920b528dccade403518b4abbd83841a3e2c9ef13e6f1a009b102"},
{"name":"state_machines-activemodel","version":"0.8.0","platform":"ruby","checksum":"e932dab190d4be044fb5f9cab01a3ea0b092c5f113d4676c6c0a0d49bf738d2c"},
{"name":"state_machines-activerecord","version":"0.8.0","platform":"ruby","checksum":"072fb701b8ab03de0608297f6c55dc34ed096e556fa8f77e556f3c461c71aab6"},

View File

@ -1836,7 +1836,7 @@ GEM
sprockets (>= 3.0.0)
ssh_data (1.3.0)
ssrf_filter (1.0.8)
stackprof (0.2.26)
stackprof (0.2.27)
state_machines (0.5.0)
state_machines-activemodel (0.8.0)
activemodel (>= 5.1)

View File

@ -12,6 +12,7 @@ import {
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_CUSTOM_FIELDS,
WIDGET_TYPE_LINKED_ITEMS,
CUSTOM_FIELDS_TYPE_NUMBER,
CUSTOM_FIELDS_TYPE_TEXT,
CUSTOM_FIELDS_TYPE_SINGLE_SELECT,
@ -136,9 +137,6 @@ export const config = {
},
},
},
LinkedWorkItemType: {
keyFields: ['linkId'],
},
WorkItem: {
fields: {
// @todo: Mocking CUSTOM_FIELDS widget while not suported by backend
@ -474,6 +472,30 @@ export const config = {
};
}
// this ensures that we dont override linkedItems.workItem when updating parent
if (incomingWidget?.type === WIDGET_TYPE_LINKED_ITEMS) {
if (!incomingWidget.linkedItems) {
return existingWidget;
}
const incomindNodes = incomingWidget.linkedItems?.nodes || [];
const existingNodes = existingWidget.linkedItems?.nodes || [];
const resultNodes = incomindNodes.map((incomingNode) => {
const existingNode =
existingNodes.find((n) => n.linkId === incomingNode.linkId) ?? {};
return { ...existingNode, ...incomingNode };
});
return {
...incomingWidget,
linkedItems: {
...incomingWidget.linkedItems,
nodes: resultNodes,
},
};
}
return { ...existingWidget, ...incomingWidget };
});
},

View File

@ -443,7 +443,11 @@ export default class MergeRequestTabs {
let newStatePathname = pathname.replace(this.actionRegex, '');
// Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'show' && this.currentAction !== 'new') {
if (
this.currentAction !== 'show' &&
this.currentAction !== 'new' &&
this.currentAction !== 'reports'
) {
newStatePathname += `/${this.currentAction}`;
}

View File

@ -1,4 +1,6 @@
<script>
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import {
BLOCKERS_ROUTE,
CODE_QUALITY_ROUTE,
@ -23,9 +25,21 @@ export default {
inject: ['hasPolicies'],
data() {
return {
blockersCounter: 2,
mr: null,
};
},
created() {
if (
window.gl?.mrWidgetData?.merge_request_cached_widget_path &&
window.gl?.mrWidgetData?.merge_request_widget_path
) {
MRWidgetService.fetchInitialData()
.then(({ data }) => {
this.mr = new MRWidgetStore({ ...window.gl.mrWidgetData, ...data });
})
.catch(() => {});
}
},
};
</script>
@ -50,7 +64,7 @@ export default {
</nav>
</aside>
<section class="md:gl-pt-5">
<router-view />
<router-view :mr="mr" />
</section>
</div>
</template>

View File

@ -1,9 +1,26 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import ReportWidgetContainer from 'ee_else_ce/vue_merge_request_widget/components/widget/app.vue';
export default {
name: 'MergeRequestReportsIndexPage',
components: {
GlLoadingIcon,
ReportWidgetContainer,
},
props: {
mr: {
type: Object,
required: false,
default: null,
},
},
};
</script>
<template>
<div></div>
<div v-if="mr">
<report-widget-container :mr="mr" reports-tab-content />
</div>
<gl-loading-icon v-else size="lg" />
</template>

View File

@ -1,11 +1,6 @@
import BlockersPage from 'ee_else_ce/merge_requests/reports/pages/blockers_page.vue';
import IndexComponent from './pages/index.vue';
import {
BLOCKERS_ROUTE,
CODE_QUALITY_ROUTE,
LICENSE_COMPLIANCE_ROUTE,
SECURITY_ROUTE,
} from './constants';
import { BLOCKERS_ROUTE } from './constants';
export default [
{
@ -14,18 +9,8 @@ export default [
component: BlockersPage,
},
{
path: '/?type=code-quality',
name: CODE_QUALITY_ROUTE,
component: IndexComponent,
},
{
path: '/?type=security',
name: SECURITY_ROUTE,
component: IndexComponent,
},
{
path: '/?type=license-compliance',
name: LICENSE_COMPLIANCE_ROUTE,
name: 'report',
path: '/:report',
component: IndexComponent,
},
];

View File

@ -221,16 +221,16 @@ export default {
/>
<div
v-if="showPlaceholder"
class="gl-filter-blur-1 gl-absolute gl-bottom-0 gl-left-0 gl-right-0 gl-top-0"
class="gl-absolute gl-bottom-0 gl-left-0 gl-right-0 gl-top-0 gl-backdrop-blur-sm"
data-testid="placeholder-overlay"
>
<div
class="gl-absolute gl-bottom-0 gl-left-0 gl-right-0 gl-top-0 gl-z-2 gl-bg-white gl-opacity-5"
class="gl-absolute gl-bottom-0 gl-left-0 gl-right-0 gl-top-0 gl-z-2 gl-bg-overlay"
></div>
<div class="gl-relative gl-z-3 gl-flex gl-h-full gl-items-center gl-justify-center">
<div class="gl-max-w-34">
<h4 data-testid="filename">{{ filename }}</h4>
<p data-testid="description">
<div class="gl-max-w-34 gl-rounded-base gl-bg-overlap gl-p-6">
<h4 data-testid="filename" class="gl-heading-4">{{ filename }}</h4>
<p data-testid="description" class="gl-mb-0 gl-text-subtle">
{{ $options.i18n.overlayMessage }}
</p>
</div>

View File

@ -13,7 +13,6 @@ import IssueToken from './issue_token.vue';
const SPACE_FACTOR = 1;
export default {
TYPE_ISSUE,
name: 'RelatedIssuableInput',
components: {
GlFormGroup,
@ -64,6 +63,11 @@ export default {
required: false,
default: false,
},
inline: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -84,6 +88,9 @@ export default {
allowAutoComplete() {
return Object.keys(this.autoCompleteSources).length > 0;
},
showDescription() {
return !this.inline && this.issuableType === TYPE_ISSUE;
},
},
mounted() {
this.setupAutoComplete();
@ -182,7 +189,7 @@ export default {
</script>
<template>
<gl-form-group>
<gl-form-group :label-class="inline ? 'gl-hidden' : ''">
<div
ref="issuableFormWrapper"
:class="{ focus: isInputFocused }"
@ -229,7 +236,7 @@ export default {
</li>
</ul>
</div>
<template v-if="issuableType === $options.TYPE_ISSUE" #description>
<template v-if="showDescription" #description>
<span :id="`${inputId}-description`">
{{
__(

View File

@ -18,33 +18,51 @@ export default {
import('~/vue_merge_request_widget/widgets/accessibility/index.vue'),
},
mixins: [glFeatureFlagsMixin()],
provide() {
return {
reportsTabContent: this.reportsTabContent,
};
},
props: {
mr: {
type: Object,
required: true,
},
reportsTabContent: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
collapsed: this.glFeatures.mrReportsTab,
collapsed: this.reportsTabContent ? false : this.glFeatures.mrReportsTab,
findingsCount: 0,
loadedCount: 0,
};
},
computed: {
testReportWidget() {
if (!this.isViewingReport('test-summary')) return undefined;
return this.mr.testResultsPath && 'MrTestReportWidget';
},
terraformPlansWidget() {
if (!this.isViewingReport('terraform')) return undefined;
return this.mr.terraformReportsPath && 'MrTerraformWidget';
},
codeQualityWidget() {
if (!this.isViewingReport('code-quality')) return undefined;
return this.mr.codequalityReportsPath ? 'MrCodeQualityWidget' : undefined;
},
accessibilityWidget() {
if (!this.isViewingReport('accessibility')) return undefined;
return this.mr.accessibilityReportPath ? 'MrAccessibilityWidget' : undefined;
},
@ -69,7 +87,17 @@ export default {
return false;
},
},
mounted() {
if (this.reportsTabContent && !this.widgets.length) {
this.$router.push({ path: '/' });
}
},
methods: {
isViewingReport(reportName) {
if (!this.reportsTabContent) return true;
return this.$router.currentRoute.params.report === reportName;
},
onLoadedReport(findings) {
this.findingsCount += findings;
this.loadedCount += 1;
@ -82,12 +110,14 @@ export default {
<section
v-if="widgets.length"
role="region"
:aria-label="__('Merge request reports')"
:aria-label="reportsTabContent ? null : __('Merge request reports')"
data-testid="mr-widget-app"
class="mr-section-container"
:class="{
'mr-section-container': !reportsTabContent,
}"
>
<state-container
v-if="glFeatures.mrReportsTab"
v-if="glFeatures.mrReportsTab && !reportsTabContent"
:status="statusIcon"
is-collapsible
collapse-on-desktop
@ -123,7 +153,8 @@ export default {
data-testid="reports-widgets-container"
class="reports-widgets-container"
:class="{
'gl-border-t gl-relative gl-border-t-section gl-bg-subtle': glFeatures.mrReportsTab,
'gl-border-t gl-relative gl-border-t-section gl-bg-subtle':
glFeatures.mrReportsTab && !reportsTabContent,
}"
>
<component
@ -132,7 +163,9 @@ export default {
:key="widget.name || index"
:mr="mr"
class="mr-widget-section"
:class="{ 'gl-border-t gl-border-t-section': index > 0 }"
:class="{
'gl-border-t gl-border-t-section': index > 0 && !reportsTabContent,
}"
@loaded="onLoadedReport"
/>
</div>

View File

@ -1,6 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { kebabCase } from 'lodash';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { logError } from '~/lib/logger';
@ -8,7 +9,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { joinPaths } from '~/lib/utils/url_utility';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { EXTENSION_ICONS } from '../../constants';
@ -50,6 +51,7 @@ export default {
SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
inject: { reportsTabContent: { default: false } },
props: {
loadingText: {
type: String,
@ -175,7 +177,7 @@ export default {
data() {
return {
isExpandedForTheFirstTime: true,
isCollapsed: true,
isCollapsed: !this.reportsTabContent,
isLoadingCollapsedContent: true,
isLoadingExpandedContent: false,
summaryError: null,
@ -212,9 +214,9 @@ export default {
return [
{
text: __('View report'),
href: mergeUrlParams(
{ type: this.widgetName.replace(WIDGET_PREFIX, '') },
href: joinPaths(
window.gl?.mrWidgetData?.reportsTabPath || '',
kebabCase(this.widgetName.replace(WIDGET_PREFIX, '')),
),
onClick(action, e) {
e.preventDefault();
@ -254,6 +256,10 @@ export default {
this.summaryError = this.errorText;
}
if (this.reportsTabContent) {
this.fetchExpandedContent();
}
this.isLoadingCollapsedContent = false;
},
methods: {
@ -341,7 +347,13 @@ export default {
<template>
<section class="media-section" data-testid="widget-extension">
<div class="gl-flex gl-px-5 gl-py-4 gl-pr-4" :class="{ 'gl-pl-9': glFeatures.mrReportsTab }">
<div
v-if="!reportsTabContent"
:class="{
'gl-pl-9': glFeatures.mrReportsTab,
'gl-flex gl-px-5 gl-py-4 gl-pr-4': !reportsTabContent,
}"
>
<status-icon
:level="glFeatures.mrReportsTab ? 2 : 1"
:name="widgetName"
@ -415,11 +427,15 @@ export default {
</div>
</div>
<div
v-if="!glFeatures.mrReportsTab && (!isCollapsed || contentError)"
class="gl-border-t gl-relative gl-border-t-section gl-bg-subtle"
v-if="!isCollapsed || contentError"
:class="{ 'gl-border-t gl-relative gl-border-t-section gl-bg-subtle': !reportsTabContent }"
data-testid="widget-extension-collapsed-section"
>
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
<div
v-if="isLoadingExpandedContent"
class="gl-text-center"
:class="{ 'report-block-container': !reportsTabContent, 'gl-py-5': reportsTabContent }"
>
<gl-loading-icon size="sm" inline /> {{ loadingText }}
</div>
<div v-else class="gl-flex gl-pl-5" :class="{ 'gl-pr-5': $scopedSlots.content }">
@ -439,7 +455,8 @@ export default {
v-if="contentWithKeyField"
:items="contentWithKeyField"
:min-item-size="32"
:style="{ maxHeight: '170px' }"
:style="{ maxHeight: reportsTabContent ? null : '170px' }"
:page-mode="glFeatures.mrReportsTab && reportsTabContent"
data-testid="dynamic-content-scroller"
class="gl-pr-5"
>

View File

@ -4,7 +4,6 @@ import { GlAlert, GlButton, GlBadge } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import { s__, n__, sprintf } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import workItemLinkedItemsQuery from '../../graphql/work_item_linked_items.query.graphql';
@ -72,8 +71,6 @@ export default {
apollo: {
linkedWorkItems: {
query: workItemLinkedItemsQuery,
fetchPolicy: fetchPolicies.NETWORK_ONLY,
nextFetchPolicy: fetchPolicies.CACHE_FIRST,
variables() {
return {
fullPath: this.workItemFullPath,

View File

@ -33,11 +33,7 @@ input[type='search'] {
/* stylelint-enable property-no-vendor-prefix */
.form-actions {
margin-top: 0;
margin-bottom: -$gl-padding;
padding: $gl-padding;
background-color: $gray-10;
border-top: 1px solid $border-color;
@apply gl-mt-0 -gl-mb-5 gl-p-5 gl-bg-subtle gl-border-t;
}
label {

View File

@ -48,7 +48,7 @@ module WorkItems
raise Gitlab::Graphql::Errors::ArgumentError, ::Types::IssuableStateEnum::INVALID_LOCKED_MESSAGE
}
argument :types,
[Types::IssueTypeEnum],
[::Types::IssueTypeEnum],
as: :issue_types,
description: 'Filter work items by the given work item types.',
required: false

View File

@ -11,7 +11,7 @@ module Types
description: 'Timestamp the link was created.', null: false,
method: :issue_link_created_at
field :work_item_state, Types::WorkItemStateEnum,
field :work_item_state, ::Types::WorkItemStateEnum,
description: 'State of the linked work item.', null: false, method: :state
field :link_id, ::Types::GlobalIDType[::WorkItems::RelatedWorkItemLink],

View File

@ -16,13 +16,13 @@ module Types
field :name, GraphQL::Types::String,
null: false,
description: 'Name of the work item type.'
field :widget_definitions, [Types::WorkItems::WidgetDefinitionInterface],
field :widget_definitions, [::Types::WorkItems::WidgetDefinitionInterface],
null: true,
description: 'Available widgets for the work item type.',
method: :widgets,
experiment: { milestone: '16.7' }
field :supported_conversion_types, [Types::WorkItems::TypeType],
field :supported_conversion_types, [::Types::WorkItems::TypeType],
null: true,
description: 'Supported conversion types for the work item type.',
experiment: { milestone: '17.8' }

View File

@ -32,11 +32,11 @@ module Types
field :has_parent, GraphQL::Types::Boolean,
null: false, method: :has_parent?, description: 'Indicates if the work item has a parent.'
field :rolled_up_counts_by_type, [Types::WorkItems::WorkItemTypeCountsByStateType],
field :rolled_up_counts_by_type, [::Types::WorkItems::WorkItemTypeCountsByStateType],
null: false, description: 'Counts of descendant work items by work item type and state.',
experiment: { milestone: '17.3' }
field :depth_limit_reached_by_type, [Types::WorkItems::WorkItemTypeDepthLimitReachedByType],
field :depth_limit_reached_by_type, [::Types::WorkItems::WorkItemTypeDepthLimitReachedByType],
null: false, description: 'Depth limit reached by allowed work item type.',
experiment: { milestone: '17.4' }

View File

@ -6,11 +6,11 @@ module Types
class LabelsUpdateInputType < BaseInputObject
graphql_name 'WorkItemWidgetLabelsUpdateInput'
argument :add_label_ids, [Types::GlobalIDType[::Label]],
argument :add_label_ids, [::Types::GlobalIDType[::Label]],
required: false,
description: 'Global IDs of labels to be added to the work item.',
prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }
argument :remove_label_ids, [Types::GlobalIDType[::Label]],
argument :remove_label_ids, [::Types::GlobalIDType[::Label]],
required: false,
description: 'Global IDs of labels to be removed from the work item.',
prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }

View File

@ -75,6 +75,10 @@ module WorkItems
has_many :allowed_parent_types_by_name, -> { order_by_name_asc },
through: :parent_restrictions, class_name: 'WorkItems::Type',
foreign_key: :parent_type_id, source: :parent_type
has_many :user_preferences,
class_name: 'WorkItems::Types::UserPreference',
primary_key: :correct_id,
inverse_of: :work_item_type
before_validation :strip_whitespace
after_save :clear_reactive_cache!

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module WorkItems
module Types
class UserPreference < ApplicationRecord
self.table_name = 'work_item_type_user_preferences'
belongs_to :user
belongs_to :namespace
belongs_to :work_item_type,
class_name: 'WorkItems::Type',
primary_key: :correct_id,
inverse_of: :user_preferences,
optional: true
end
end
end

View File

@ -20,6 +20,8 @@ module Ci
calculate_aggregate_duration_percentiles(query, result)
ServiceResponse.success(payload: { aggregate: result })
rescue ::ClickHouse::Client::DatabaseError => e
ServiceResponse.error(message: e.message)
end
def calculate_aggregate_count(query, result)
@ -34,7 +36,7 @@ module Ci
query = query
.select(:status, query.count_pipelines_function.as('count'))
.by_status(status_groups.flat_map(&STATUS_GROUP_TO_STATUSES).compact)
.by_status(selected_statuses)
.group_by_status
result_by_status = ::ClickHouse::Client.select(query.to_sql, :main).map(&:values).to_h

View File

@ -61,6 +61,10 @@ module Ci
duration_percentiles.map { |p| :"p#{p}" }
end
def selected_statuses
status_groups.flat_map(&STATUS_GROUP_TO_STATUSES).compact
end
def validate_arguments
if (duration_percentiles & ALLOWED_PERCENTILES) != duration_percentiles
return ServiceResponse.error(message: 'Invalid duration percentiles specified')

View File

@ -35,6 +35,11 @@ module Ci
time_series = create_empty_time_series
query = base_query
if status_groups.any?
calculate_time_series_count(query, time_series)
calculate_time_series_status_group_counts(query, time_series)
end
calculate_time_series_duration_percentiles(query, time_series)
collect_metrics
@ -43,12 +48,49 @@ module Ci
{ label: date, **value }
end
})
rescue ::ClickHouse::Client::DatabaseError => e
ServiceResponse.error(message: e.message)
end
def timespan_bin_query_function(query)
query.timestamp_bin_function(time_series_period)
end
def calculate_time_series_count(query, time_series)
return if status_groups.exclude?(:any)
all_query = query
.select(timespan_bin_query_function(query), query.count_pipelines_function.as('all'))
.group_by_timestamp_bin
all_count_result =
execute_select_query(all_query)
.to_h do |entry|
[parse_in_utc(entry[:timestamp]), { count: { any: entry[:all] } }]
end
time_series.deep_merge!(all_count_result)
end
def calculate_time_series_status_group_counts(query, time_series)
return unless status_groups.intersect?(STATUS_GROUPS)
query = query
.select(timespan_bin_query_function(query), :status, query.count_pipelines_function.as('count'))
.by_status(selected_statuses)
.group_by_timestamp_bin
.group_by_status
# Produce a chronological set of hashes
# such as `{ Time.utc(2023, 1, 1) => { count: { success: 1, failed: 0, other: 1, any: 3 } }`
counts_by_timestamp =
execute_select_query(query)
.group_by { |entry| parse_in_utc(entry[:timestamp]) }
.transform_values { |counts_by_status| { count: group_and_sum_counts(counts_by_status) } }
time_series.deep_merge!(counts_by_timestamp)
end
def calculate_time_series_duration_percentiles(query, time_series)
return if duration_percentiles.empty?
@ -56,11 +98,9 @@ module Ci
timespan_bin_query_function(query),
*duration_percentiles.map { |p| query.duration_quantile_function(p) }
).group_by_timestamp_bin
duration_by_date_result = ::ClickHouse::Client.select(duration_by_date_query.to_sql, :main)
time_series.deep_merge!(
duration_by_date_result
.map(&:symbolize_keys)
execute_select_query(duration_by_date_query)
.group_by { |entry| parse_in_utc(entry[:timestamp]) }
.transform_values { |hash| hash.sole.excluding(:timestamp) } # Keep only percentiles
.transform_values do |percentiles_by_date|
@ -97,6 +137,7 @@ module Ci
{}.tap do |time_series|
while current < to_time
time_series[current] = {
count: status_groups.index_with(0),
duration_statistics: round_percentiles(duration_percentile_symbols.index_with(0))
}.compact_blank
@ -117,6 +158,24 @@ module Ci
to_utc(Time.parse(timestamp)) # rubocop:disable Rails/TimeZone -- false positive, to_utc takes care of this
end
def group_and_sum_counts(counts_by_status_and_time)
# This method receives an array of hashes representing counts per job status per day,
# e.g. [
# { :timestamp => "2023-01-01 00:00:00", :status => "canceled", :count => 2 },
# { :timestamp => "2023-01-01 00:00:00", :status => "skipped", :count => 1 },
# { :timestamp => "2023-01-01 00:00:00", :status => "success", :count => 2 }
# ] and returns a hash synthesizing the information as
# { 2023-01-01 00:00:00 UTC => {:count => { :other=>3, :success=>2 } } }
counts_by_status_and_time
.to_h { |h| h.slice(:status, :count).values } # Create hash from status to count
.group_by { |status, _count| STATUS_TO_STATUS_GROUP[status] } # Group by status group
.transform_values { |pairs| pairs.sum(&:last) } # Sum counts from all statuses in group
end
def execute_select_query(query)
::ClickHouse::Client.select(query.to_sql, :main).map(&:symbolize_keys)
end
def collect_metrics
track_internal_event(
'collect_time_series_pipeline_analytics',

View File

@ -2,11 +2,11 @@
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h1.page-title.gl-text-size-h-display.gl-flex.gl-items-center
.gl-flex.gl-items-center.gl-justify-center
= sprite_icon('tanuki', css_class: 'gl-mr-3', size: 48)
= _('Import an exported GitLab project')
%hr
= render ::Layouts::PageHeadingComponent.new('') do |c|
- c.with_heading do
%span.gl-inline-flex.gl-items-center.gl-gap-3
= sprite_icon('tanuki', size: 32)
= _('Import an exported GitLab project')
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
= render 'import/shared/new_project_form'
@ -20,7 +20,7 @@
.form-group
= file_field_tag :file, class: ''
.row
.form-actions.col-sm-12
.col-sm-12.gl-mt-5
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { testid: 'import-project-button' }}) do
= _('Import project')
= render Pajamas::ButtonComponent.new(href: new_project_path) do

View File

@ -761,6 +761,8 @@
- 1
- - requirements_management_process_requirements_reports
- 1
- - sbom_create_occurrences_vulnerabilities
- 1
- - sbom_process_transfer_events
- 1
- - sbom_process_vulnerabilities

View File

@ -4,12 +4,7 @@ classes:
- Ci::RunningBuild
feature_categories:
- continuous_integration
description: Running builds metadata. Despite the generic `RunningBuild` name, in
this first iteration it applies only to shared runners. The decision to insert all
of the running builds here was deferred to avoid the pressure on the database as
at this time that was not necessary. We can reconsider the decision to limit this
only to shared runners when there is more evidence that inserting all of the running
builds there is worth the additional pressure.
description: Running builds metadata.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62912
milestone: '14.0'
gitlab_schema: gitlab_ci

View File

@ -8,14 +8,6 @@ gitlab_schema: gitlab_main_cell
description: An Experiment Metadata record holds extra information about the experiment
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104267
milestone: '15.7'
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: experiment_id
table: ml_experiments
sharding_key: project_id
belongs_to: experiment
desired_sharding_key_migration_job_name: BackfillMlExperimentMetadataProjectId
table_size: small
sharding_key:
project_id: projects

View File

@ -0,0 +1,12 @@
---
table_name: work_item_type_user_preferences
classes:
- WorkItems::Types::UserPreference
feature_categories:
- team_planning
description: User preferences per work item type and namespace
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/176869
milestone: '17.8'
gitlab_schema: gitlab_main_cell
sharding_key:
namespace_id: namespaces

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
class CreateWorkItemTypeUserPreferences < Gitlab::Database::Migration[2.2]
milestone '17.9'
UNIQUE_INDEX_NAME = 'uniq_preference_by_user_namespace_and_work_item_type'
# rubocop:disable Migration/EnsureFactoryForTable -- https://gitlab.com/gitlab-org/gitlab/-/issues/468630
# the factory exist in spec/factories/work_items/user_preference.rb
def change
create_table :work_item_type_user_preferences do |t|
t.timestamps_with_timezone null: false
t.bigint :user_id, null: false
t.bigint :namespace_id, null: false
t.bigint :work_item_type_id
t.text :sort, limit: 255
t.index %i[user_id namespace_id work_item_type_id], name: UNIQUE_INDEX_NAME
end
end
# rubocop:enable Migration/EnsureFactoryForTable
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
class AddForeignKeysToWorkItemTypeUserPreferences < Gitlab::Database::Migration[2.2]
milestone '17.9'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :work_item_type_user_preferences,
:users,
column: :user_id,
on_delete: :cascade
add_concurrent_index :work_item_type_user_preferences, :namespace_id
add_concurrent_foreign_key :work_item_type_user_preferences,
:namespaces,
column: :namespace_id,
on_delete: :cascade
add_concurrent_index :work_item_type_user_preferences, :work_item_type_id
add_concurrent_foreign_key :work_item_type_user_preferences,
:work_item_types,
target_column: :correct_id,
column: :work_item_type_id,
on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key_if_exists :work_item_type_user_preferences, :users, column: :user_id
remove_foreign_key_if_exists :work_item_type_user_preferences, :namespaces, column: :namespace_id
remove_foreign_key_if_exists :work_item_type_user_preferences, :work_item_types, column: :work_item_type_id
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddMlExperimentMetadataProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.9'
def up
add_not_null_constraint :ml_experiment_metadata, :project_id
end
def down
remove_not_null_constraint :ml_experiment_metadata, :project_id
end
end

View File

@ -0,0 +1 @@
96f5050ea1850a89793874cae18ee6cb041e2f22c16ecd50958cd6e0ff989478

View File

@ -0,0 +1 @@
806872796ca346c4a6a62442798b2be02602a3a3cf4e42816d6bc3240075e5c5

View File

@ -0,0 +1 @@
4c5c6a0805f9e43ef76364ce1282c88b2a334dbefc6e50b1fe95f0dd4ec85d18

View File

@ -16102,7 +16102,8 @@ CREATE TABLE ml_experiment_metadata (
value text NOT NULL,
project_id bigint,
CONSTRAINT check_112fe5002d CHECK ((char_length(name) <= 255)),
CONSTRAINT check_a91c633d68 CHECK ((char_length(value) <= 5000))
CONSTRAINT check_a91c633d68 CHECK ((char_length(value) <= 5000)),
CONSTRAINT check_ca9b8315ef CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE ml_experiment_metadata_id_seq
@ -23475,6 +23476,26 @@ CREATE SEQUENCE work_item_type_custom_fields_id_seq
ALTER SEQUENCE work_item_type_custom_fields_id_seq OWNED BY work_item_type_custom_fields.id;
CREATE TABLE work_item_type_user_preferences (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
user_id bigint NOT NULL,
namespace_id bigint NOT NULL,
work_item_type_id bigint,
sort text,
CONSTRAINT check_7f4a25cee7 CHECK ((char_length(sort) <= 255))
);
CREATE SEQUENCE work_item_type_user_preferences_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE work_item_type_user_preferences_id_seq OWNED BY work_item_type_user_preferences.id;
CREATE TABLE work_item_types (
id bigint NOT NULL,
base_type smallint DEFAULT 0 NOT NULL,
@ -25610,6 +25631,8 @@ ALTER TABLE ONLY work_item_text_field_values ALTER COLUMN id SET DEFAULT nextval
ALTER TABLE ONLY work_item_type_custom_fields ALTER COLUMN id SET DEFAULT nextval('work_item_type_custom_fields_id_seq'::regclass);
ALTER TABLE ONLY work_item_type_user_preferences ALTER COLUMN id SET DEFAULT nextval('work_item_type_user_preferences_id_seq'::regclass);
ALTER TABLE ONLY work_item_widget_definitions ALTER COLUMN id SET DEFAULT nextval('work_item_widget_definitions_id_seq'::regclass);
ALTER TABLE ONLY workspace_variables ALTER COLUMN id SET DEFAULT nextval('workspace_variables_id_seq'::regclass);
@ -28663,6 +28686,9 @@ ALTER TABLE ONLY work_item_text_field_values
ALTER TABLE ONLY work_item_type_custom_fields
ADD CONSTRAINT work_item_type_custom_fields_pkey PRIMARY KEY (id);
ALTER TABLE ONLY work_item_type_user_preferences
ADD CONSTRAINT work_item_type_user_preferences_pkey PRIMARY KEY (id);
ALTER TABLE ONLY work_item_types
ADD CONSTRAINT work_item_types_pkey PRIMARY KEY (id);
@ -34912,6 +34938,10 @@ CREATE INDEX index_work_item_type_custom_fields_on_custom_field_id ON work_item_
CREATE INDEX index_work_item_type_custom_fields_on_work_item_type_id ON work_item_type_custom_fields USING btree (work_item_type_id);
CREATE INDEX index_work_item_type_user_preferences_on_namespace_id ON work_item_type_user_preferences USING btree (namespace_id);
CREATE INDEX index_work_item_type_user_preferences_on_work_item_type_id ON work_item_type_user_preferences USING btree (work_item_type_id);
CREATE INDEX index_work_item_types_on_base_type_and_id ON work_item_types USING btree (base_type, id);
CREATE UNIQUE INDEX index_work_item_types_on_correct_id_unique ON work_item_types USING btree (correct_id);
@ -35284,6 +35314,8 @@ CREATE UNIQUE INDEX uniq_pkgs_debian_project_distributions_project_id_and_codena
CREATE UNIQUE INDEX uniq_pkgs_debian_project_distributions_project_id_and_suite ON packages_debian_project_distributions USING btree (project_id, suite);
CREATE INDEX uniq_preference_by_user_namespace_and_work_item_type ON work_item_type_user_preferences USING btree (user_id, namespace_id, work_item_type_id);
CREATE UNIQUE INDEX unique_amazon_s3_configurations_namespace_id_and_bucket_name ON audit_events_amazon_s3_configurations USING btree (namespace_id, bucket_name);
CREATE UNIQUE INDEX unique_amazon_s3_configurations_namespace_id_and_name ON audit_events_amazon_s3_configurations USING btree (namespace_id, name);
@ -37764,6 +37796,9 @@ ALTER TABLE ONLY merge_requests
ALTER TABLE ONLY clusters_managed_resources
ADD CONSTRAINT fk_068dba90c3 FOREIGN KEY (cluster_agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE;
ALTER TABLE ONLY work_item_type_user_preferences
ADD CONSTRAINT fk_0748f95f41 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY sbom_occurrences_vulnerabilities
ADD CONSTRAINT fk_07b81e3a81 FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
@ -38055,6 +38090,9 @@ ALTER TABLE ONLY deployment_approvals
ALTER TABLE ONLY audit_events_instance_external_audit_event_destinations
ADD CONSTRAINT fk_2d3ebd0fbc FOREIGN KEY (stream_destination_id) REFERENCES audit_events_instance_external_streaming_destinations(id) ON DELETE SET NULL;
ALTER TABLE ONLY work_item_type_user_preferences
ADD CONSTRAINT fk_2e37b4f066 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY notes
ADD CONSTRAINT fk_2e82291620 FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE SET NULL;
@ -38571,6 +38609,9 @@ ALTER TABLE ONLY topics
ALTER TABLE ONLY work_item_text_field_values
ADD CONSTRAINT fk_79c719630f FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY work_item_type_user_preferences
ADD CONSTRAINT fk_79e0353950 FOREIGN KEY (work_item_type_id) REFERENCES work_item_types(correct_id) ON DELETE CASCADE;
ALTER TABLE ONLY packages_maven_metadata
ADD CONSTRAINT fk_7a170ee0a3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -83,6 +83,29 @@ You can add parameters to block definitions:
Markdown does not support any parameters, and always uses PNG format.
## Include diagram files
You can include or embed a PlantUML diagram from separate files in the repository using
the `include` directive. Use this to maintain complex diagrams in dedicated files, or to
reuse diagrams. For example:
- **Markdown**:
````markdown
```plantuml
::include{file=diagram.puml}
```
````
- **AsciiDoc**:
```plaintext
[plantuml, format="png", id="myDiagram", width="200px"]
----
include::diagram.puml[]
----
```
## Configure your PlantUML server
Before you can enable PlantUML in GitLab, set up your own PlantUML

View File

@ -25,29 +25,34 @@ Install one of the following GitLab-approved LLM models:
<!-- vale gitlab_base.Spelling = NO -->
| Model family | Model | Code completion | Code generation | GitLab Duo Chat |
|--------------|------------------------------------------------------------------------------------|-----------------|-----------------|---------|
| Mistral Codestral | [Codestral 22B v0.1](https://huggingface.co/mistralai/Codestral-22B-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
| Mistral | [Mistral 7B-it v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Mistral | [Mixtral 8x7B-it v0.1](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Mistral | [Mixtral 8x22B-it v0.1](https://huggingface.co/mistralai/Mixtral-8x22B-Instruct-v0.1) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Claude 3 | [Claude 3.5 Sonnet](https://www.anthropic.com/news/claude-3-5-sonnet) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| GPT | [GPT-4 Turbo](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-4) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| GPT | [GPT-4o](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-4o-and-gpt-4-turbo) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| GPT | [GPT-4o-mini](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-4o-and-gpt-4-turbo) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Model family | Model | Supported platforms | Status | Code completion | Code generation | GitLab Duo Chat |
|--------------|-------|---------------------|--------|-----------------|-----------------|-----------------|
|Mistral Codestral | [Codestral 22B v0.1](https://huggingface.co/mistralai/Codestral-22B-v0.1) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Generally available | 🟢 Green | 🟢 Green | N/A |
| Mistral | [Mistral 7B-it v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Generally available | 🟢 Green | 🟢 Green | 🔴 Red |
| Mistral | [Mixtral 8x7B-it v0.1](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) <br> [AWS Bedrock](https://aws.amazon.com/bedrock/mistral/) | Generally available | 🟢 Green | 🟢 Green | 🟡 Amber |
| Mistral | [Mixtral 8x22B-it v0.1](https://huggingface.co/mistralai/Mixtral-8x22B-Instruct-v0.1) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Generally available | 🟢 Green | 🟢 Green | 🟢 Green |
| Claude 3 | [Claude 3.5 Sonnet](https://www.anthropic.com/news/claude-3-5-sonnet) | [AWS Bedrock](https://aws.amazon.com/bedrock/claude/) | Generally available | 🟢 Green | 🟢 Green | 🟢 Green |
| GPT | [GPT-4 Turbo](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-4) | [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview) | Generally available | 🟢 Green | 🟢 Green | 🟡 Amber |
| GPT | [GPT-4o](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-4o-and-gpt-4-turbo) | [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview) | Generally available | 🟢 Green | 🟢 Green | 🟢 Green |
| GPT | [GPT-4o-mini](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models?tabs=python-secure#gpt-4o-and-gpt-4-turbo) | [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview) | Generally available | 🟢 Green | 🟢 Green | 🟡 Amber |
Legend:
- 🟢 Green - Strongly recommended. The model can handle the feature without any loss of quality.
- 🟡 Amber - Recommended. The model supports the feature, but there might be minor compromises or limitations.
- 🔴 Red - Not recommended. The model is unsuitable for the feature, likely resulting in significant quality loss or performance issues.
The following models are under evaluation, and support is limited:
| Model family | Model | Code completion | Code generation | GitLab Duo Chat |
|--------------- |---------------------------------------------------------------------|-----------------|-----------------|---------|
| CodeGemma | [CodeGemma 2b](https://huggingface.co/google/codegemma-2b) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| CodeGemma | [CodeGemma 7b-it](https://huggingface.co/google/codegemma-7b-it) | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| CodeGemma | [CodeGemma 7b-code](https://huggingface.co/google/codegemma-7b) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Code Llama | [Code-Llama 13b-code](https://huggingface.co/meta-llama/CodeLlama-13b-hf) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Code Llama | [Code-Llama 13b](https://huggingface.co/meta-llama/CodeLlama-13b-Instruct-hf) | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| DeepSeek Coder | [DeepSeek Coder 33b Instruct](https://huggingface.co/deepseek-ai/deepseek-coder-33b-instruct) | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
| DeepSeek Coder | [DeepSeek Coder 33b Base](https://huggingface.co/deepseek-ai/deepseek-coder-33b-base) | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Mistral | [Mistral 7B-it v0.2](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Model family | Model | Supported platforms | Status | Code completion | Code generation | GitLab Duo Chat |
|--------------- |-------|---------------------|--------|-----------------|-----------------|-----------------|
| CodeGemma | [CodeGemma 2b](https://huggingface.co/google/codegemma-2b) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Beta | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| CodeGemma | [CodeGemma 7b-it](https://huggingface.co/google/codegemma-7b-it) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Beta | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| CodeGemma | [CodeGemma 7b-code](https://huggingface.co/google/codegemma-7b) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Beta | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Code Llama | [Code-Llama 13b](https://huggingface.co/meta-llama/CodeLlama-13b-Instruct-hf) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Beta | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No |
| DeepSeek Coder | [DeepSeek Coder 33b Instruct](https://huggingface.co/deepseek-ai/deepseek-coder-33b-instruct) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Beta | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No |
| DeepSeek Coder | [DeepSeek Coder 33b Base](https://huggingface.co/deepseek-ai/deepseek-coder-33b-base) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) | Beta | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No |
| Mistral | [Mistral 7B-it v0.2](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2) | [vLLM](supported_llm_serving_platforms.md#for-self-hosted-model-deployments) <br> [AWS Bedrock](https://aws.amazon.com/bedrock/mistral/) | Beta | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
<!-- vale gitlab_base.Spelling = YES -->

View File

@ -73,54 +73,14 @@ The option is not available when an artifact has no expiry set.
By default, the [latest artifacts are always kept](#keep-artifacts-from-most-recent-successful-jobs).
### With a dynamically defined name
### With an explicitly defined artifact name
You can use [CI/CD variables](../variables/index.md) to dynamically define the
artifacts file's name.
For example, to create an archive with a name of the current job:
You can explicitly customize artifact names using the [`artifacts:name`](../yaml/index.md#artifactsname) configuration:
```yaml
job:
artifacts:
name: "$CI_JOB_NAME"
paths:
- binaries/
```
To create an archive with a name of the current branch or tag including only
the binaries directory:
```yaml
job:
artifacts:
name: "$CI_COMMIT_REF_NAME"
paths:
- binaries/
```
If your branch-name contains forward slashes
(for example `feature/my-feature`) use `$CI_COMMIT_REF_SLUG`
instead of `$CI_COMMIT_REF_NAME` for proper naming of the artifact.
### With a Windows runner or shell executor
If you use Windows Batch to run your shell scripts you must replace `$` with `%`:
```yaml
job:
artifacts:
name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
paths:
- binaries/
```
If you use Windows PowerShell to run your shell scripts you must replace `$` with `$env:`:
```yaml
job:
artifacts:
name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
name: "job1-artifacts-file"
paths:
- binaries/
```
@ -179,6 +139,34 @@ artifacts:
- "*.txt"
```
### With variable expansion
Variable expansion is supported for:
- [`artifacts:name`](../yaml/index.md#artifactsname)
- [`artifacts:paths`](../yaml/index.md#artifactspaths)
- [`artifacts:exclude`](../yaml/index.md#artifactsexclude)
Instead of using shell, GitLab Runner uses its
[internal variable expansion mechanism](../variables/where_variables_can_be_used.md#gitlab-runner-internal-variable-expansion-mechanism).
Only [CI/CD variables](../variables/index.md) are supported in this context.
For example, to create an archive using the current branch or tag name
including only files from a directory named after the current project:
```yaml
job:
artifacts:
name: "$CI_COMMIT_REF_NAME"
paths:
- binaries/${CI_PROJECT_NAME}/"
```
When your branch name contains forward slashes (for example, `feature/my-feature`),
use `$CI_COMMIT_REF_SLUG` instead of `$CI_COMMIT_REF_NAME` to ensure proper artifact naming.
Variables are expanded before [globs](https://en.wikipedia.org/wiki/Glob_(programming)).
## Fetching artifacts
By default, jobs fetch all artifacts from jobs defined in previous stages. These artifacts are downloaded into the job's working directory.

View File

@ -966,7 +966,7 @@ job1:
The metadata renders in a plain text `.json` file stored with the artifact. The
filename is `{ARTIFACT_NAME}-metadata.json`. `ARTIFACT_NAME` is the
[name for the artifact](../jobs/job_artifacts.md#with-a-dynamically-defined-name)
[name for the artifact](../jobs/job_artifacts.md#with-an-explicitly-defined-artifact-name)
defined in the `.gitlab-ci.yml` file. If the name is not defined, the default filename is
`artifacts-metadata.json`.

View File

@ -32,6 +32,7 @@ There are two places defined variables can be used. On the:
| [`after_script`](../yaml/index.md#after_script) | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment). |
| [`artifacts:name`](../yaml/index.md#artifactsname) | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). |
| [`artifacts:paths`](../yaml/index.md#artifactspaths) | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). |
| [`artifacts:exclude`](../yaml/index.md#artifactsexclude) | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). |
| [`before_script`](../yaml/index.md#before_script) | yes | Script execution shell | The variable expansion is made by the [execution shell environment](#execution-shell-environment) |
| [`cache:key`](../yaml/index.md#cachekey) | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). |
| [`cache:paths`](../yaml/index.md#cachepaths) | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism). |
@ -107,6 +108,11 @@ the expansion is done only once, so nested variables may or may not work, depend
ordering of variables definitions, and whether [nested variable expansion](#nested-variable-expansion)
is enabled in GitLab.
For artifacts and cache uploads, the runner uses
[mvdan.cc/sh/v3/expand](https://pkg.go.dev/mvdan.cc/sh/v3/expand) for variable
expansion instead of Go's `os.Expand()` because `mvdan.cc/sh/v3/expand` supports
[parameter expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html).
### Execution shell environment
This is an expansion phase that takes place during the `script` execution.

View File

@ -1459,7 +1459,7 @@ job:
**Related topics**:
- [Use CI/CD variables to define the artifacts name](../jobs/job_artifacts.md#with-a-dynamically-defined-name).
- [Use CI/CD variables to define the artifacts configuration](../jobs/job_artifacts.md#with-variable-expansion)
#### `artifacts:public`

View File

@ -14,6 +14,9 @@ At a high level, to add a new relation to the direct transfer importer, you must
1. Add a label for the newly created relation to display in the UI.
1. Ensure sufficient test coverage.
NOTE:
To mitigate the risk of introducing bugs and performance issues, newly added relations should be put behind a feature flag.
## Export from source
There are a few types of relations we export:

View File

@ -6,6 +6,9 @@ info: Any user with at least the Maintainer role can merge updates to this conte
# Import/Export development documentation
NOTE:
To mitigate the risk of introducing bugs and performance issues, newly added relations should be put behind a feature flag.
General development guidelines and tips for the [Import/Export feature](../user/project/settings/import_export.md).
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> This document is originally based on the [Import/Export 201 presentation available on YouTube](https://www.youtube.com/watch?v=V3i1OfExotE).

View File

@ -130,7 +130,7 @@ deploy-pages: # a user-defined job that builds your pages and saves them to the
`rules`, you're checking that this commit was made on the default branch. Typically, you wouldn't want to build and
deploy the live site from another branch.
You don't need to add anything else to this file. When you're ready, select **Commit changes** at the bottom of the page.
You don't need to add anything else to this file. When you're ready, select **Commit changes** at the top of the page.
You've just triggered a pipeline to build your Hugo site!
@ -142,7 +142,7 @@ From the left-hand navigation, select **Build > Pipelines**.
You'll see that GitLab has run your `test` and `deploy-pages` jobs.
To view your site, on the left-hand navigation, select **Settings > Pages**
To view your site, on the left-hand navigation, select **Deploy > Pages**
The `pages` job in your pipeline has deployed the contents of your `public` directory to GitLab Pages. Under **Access pages**, you should see the link in the format: `https://<your-namespace>.gitlab.io/<project-path>`.
@ -152,7 +152,7 @@ Select the displayed link to view your site.
When you first view your Hugo site, the stylesheet won't work. Don't worry, you need to make a small change in your Hugo configuration file. Hugo needs to know the URL of your GitLab Pages site so it can build relative links to stylesheets and other assets:
1. In your local Hugo site, open your `config.yaml` or `config.toml` file.
1. In your local Hugo site, pull the latest changes, and open your `config.yaml` or `config.toml` file.
1. Change the value of the `BaseURL` parameter to match the URL that appears in your GitLab Pages settings.
1. Push your changed file to GitLab, and your pipeline is triggered again.

View File

@ -247,6 +247,14 @@ pages, change the filename from `.adoc` to `.asciidoc`.
include::basics.adoc[]
```
```plaintext
// you can also include other files from you repository
[,language]
----
include::my_code_file.language[]
----
```
To guarantee good system performance and prevent malicious documents from causing
problems, GitLab enforces a maximum limit on the number of include directives
processed in any one document. By default, a document can have up to 32 include directives, which is
@ -504,6 +512,15 @@ Bob -> Alice : hello
----
```
To include PlantUML diagrams stored in separate files:
```plaintext
[plantuml, format="png", id="myDiagram", width="200px"]
----
include::diagram.puml[]
----
```
### Multimedia
```plaintext

View File

@ -1019,6 +1019,20 @@ graph TB
PlantUML integration is enabled on GitLab.com. To make PlantUML available in self-managed
installation of GitLab, a GitLab administrator [must enable it](../administration/integration/plantuml.md).
After you enable PlantUML, diagram delimiters `@startuml`/`@enduml` aren't required, as these
are replaced by the `plantuml` block. For example:
````markdown
```plantuml
Bob -> Alice : hello
Alice -> Bob : hi
```
````
You can include or embed a PlantUML diagram from separate files in the repository using
the `::include` directive.
For more information, see [Include diagtram files](../administration/integration/plantuml.md#include-diagram-files).
### Kroki
To make Kroki available in GitLab, a GitLab administrator must enable it.
@ -1447,28 +1461,28 @@ display a color chip next to the color code. For example:
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#emoji).
Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/monkey.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":monkey:" alt=":monkey:">
around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/star2.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":star2:" alt=":star2:">
to your <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":speech_balloon:" alt=":speech_balloon:">.
Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/monkey.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":monkey:" alt=":monkey:">
around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/star2.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":star2:" alt=":star2:">
to your <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":speech_balloon:" alt=":speech_balloon:">.
Well we have a gift for you:
<img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/zap.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":zap:" alt=":zap:">
<img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/zap.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":zap:" alt=":zap:">
You can use emoji anywhere GitLab Flavored Markdown is supported.
<img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/v.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":v:" alt=":v:">
<img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/v.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":v:" alt=":v:">
You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/bug.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":bug:" alt=":bug:">
or warn about <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":speak_no_evil:" alt=":speak_no_evil:">
patches. If someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/snail.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":snail:" alt=":snail:">
code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/birthday.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":birthday:" alt=":birthday:">.
People <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/heart.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":heart:" alt=":heart:">
You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/bug.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":bug:" alt=":bug:">
or warn about <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":speak_no_evil:" alt=":speak_no_evil:">
patches. If someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/snail.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":snail:" alt=":snail:">
code, send them some <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/birthday.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":birthday:" alt=":birthday:">.
People <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/heart.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":heart:" alt=":heart:">
you for that.
If you're new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/fearful.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":fearful:" alt=":fearful:">.
You can join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/family.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":family:" alt=":family:">.
If you're new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/fearful.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":fearful:" alt=":fearful:">.
You can join the emoji <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/family.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":family:" alt=":family:">.
Just look up one of the supported codes.
Consult the [TanukiEmoji reference](https://gitlab-org.gitlab.io/ruby/gems/tanuki_emoji/) for a list
of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/4/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":thumbsup:" alt=":thumbsup:">
of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab/-/raw/master/public/-/emojis/4/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":thumbsup:" alt=":thumbsup:">
The above paragraphs in raw Markdown:
@ -1628,6 +1642,35 @@ To use includes from separate wiki pages or external URLs, administrators can en
::include{file=https://example.org/installation.md}
```
### Use includes in code blocks
You can use the `::include` directive inside code blocks to add content from files in your repository.
For example, if your repository contains a file `javascript_code.js`:
```javascript
var s = "JavaScript syntax highlighting";
alert(s);
```
You can include it in your Markdown file:
````markdown
Our script contains:
```javascript
::include{file=javascript_code.js}
```
````
The content renders as:
Our script contains:
```javascript
var s = "JavaScript syntax highlighting";
alert(s);
```
## Escape characters
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#escape-characters).

View File

@ -24,13 +24,7 @@ module Gitlab
def cached_application_settings
return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
begin
::ApplicationSetting.cached
rescue StandardError
# In case Redis isn't running
# or the Redis UNIX socket file is not available
# or the DB is not running (we use migrations in the cache key)
end
::ApplicationSetting.cached
end
def uncached_application_settings

View File

@ -52461,6 +52461,9 @@ msgstr ""
msgid "SecurityReports|All tools"
msgstr ""
msgid "SecurityReports|Attach to existing issue"
msgstr ""
msgid "SecurityReports|CVSS v%{version}:"
msgstr ""

View File

@ -198,6 +198,21 @@ function setup_db_praefect() {
function setup_db() {
section_start "setup-db" "Setting up DBs"
if [[ -f pg_dumpall.sql ]] && ! [[ "$DECOMPOSED_DB" =~ "false" ]]; then
echo "Found pg_dumpall.sql, applying!"
psql -h postgres -U postgres -q < pg_dumpall.sql > /dev/null
rm pg_dumpall.sql
section_end "setup-db"
return 0
fi
if [[ -f pg_dumpall.sql ]]; then
echo "Found pg_dumpall.sql but we're not using a standard multi-db (decomposed) setup. Performing a regular db setup instead."
rm pg_dumpall.sql
fi
setup_db_user_only
run_timed_command_with_metric "bundle exec rake db:drop db:create db:schema:load db:migrate gitlab:db:lock_writes" "setup_db"
setup_db_praefect

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :work_item_user_preference, class: 'WorkItems::UserPreference' do
association :user
association :namespace
association :work_item_type
end
end

View File

@ -1,3 +1,4 @@
import { GlFormGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import GfmAutoComplete from '~/gfm_auto_complete';
@ -14,7 +15,7 @@ describe('RelatedIssuableInput', () => {
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
};
const mountComponent = (props = {}) => {
const mountComponent = (props = {}, stubs) => {
wrapper = shallowMount(RelatedIssuableInput, {
propsData: {
inputValue: '',
@ -25,6 +26,7 @@ describe('RelatedIssuableInput', () => {
...props,
},
attachTo: document.body,
stubs,
});
};
@ -96,4 +98,28 @@ describe('RelatedIssuableInput', () => {
]);
});
});
describe('description', () => {
const findDescription = () => wrapper.find('span');
it('shows description text', () => {
mountComponent(undefined, { GlFormGroup });
expect(findDescription().text()).toBe(
'Only issues can be linked from this form. You can also link this issue from an epic or task.',
);
});
it('hides description when inline prop is true', () => {
mountComponent({ inline: true }, { GlFormGroup });
expect(findDescription().exists()).toBe(false);
});
it('hides description when issuableType is not TYPE_ISSUE', () => {
mountComponent({ issuableType: 'MergeRequest' }, { GlFormGroup });
expect(findDescription().exists()).toBe(false);
});
});
});

View File

@ -513,7 +513,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
expect(findActionButtons().props('tertiaryButtons')).toEqual([
expect.objectContaining({ href: 'reportsTabPath?type=Test', text: 'View report' }),
expect.objectContaining({ href: 'reportsTabPath/test', text: 'View report' }),
]);
});
@ -531,11 +531,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
await nextTick();
expect(window.mrTabs.tabShown).toHaveBeenCalledWith('reports');
expect(window.history.replaceState).toHaveBeenCalledWith(
null,
null,
'reportsTabPath?type=Test',
);
expect(window.history.replaceState).toHaveBeenCalledWith(null, null, 'reportsTabPath/test');
});
});
});

View File

@ -166,7 +166,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
end
it 'does not have new organization menu item' do
expect(view_model[:menu_sections]).to match_array([])
expect(view_model[:menu_sections]).to be_empty
end
end
@ -176,7 +176,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
end
it 'does not have new organization menu item' do
expect(view_model[:menu_sections]).to match_array([])
expect(view_model[:menu_sections]).to be_empty
end
end
end

View File

@ -111,7 +111,7 @@ RSpec.describe UsersHelper, feature_category: :user_management do
end
it 'is empty' do
expect(profile_actions).to match_array []
expect(profile_actions).to be_empty
end
end

View File

@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :markdown do
include RepoHelpers
using RSpec::Parameterized::TableSyntax
it_behaves_like 'sanitize pipeline'
@ -259,4 +260,59 @@ RSpec.describe Banzai::Pipeline::FullPipeline, feature_category: :markdown do
end
end
end
describe 'when using include in code segements' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:ref) { 'markdown' }
let_it_be(:requested_path) { '/' }
let_it_be(:commit) { project.commit(ref) }
let_it_be(:context) do
{
commit: commit,
project: project,
ref: ref,
text_source: :blob,
requested_path: requested_path,
no_sourcepos: true
}
end
let_it_be(:project_files) do
{
'diagram.puml' => "@startuml\nBob -> Sara : Hello\n@enduml",
'code.yaml' => "---\ntest: true"
}
end
let(:input) do
<<~MD
```plantuml
::include{file=diagram.puml}
```
```yaml
::include{file=code.yaml}
```
MD
end
around do |example|
create_and_delete_files(project, project_files, branch_name: ref) do
example.run
end
end
subject(:output) { described_class.call(input, context)[:output].to_html }
it 'renders PlanUML' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
is_expected.to include 'http://localhost:8080/png/U9npA2v9B2efpStXSifFKj2rKmXEB4fKi5BmICt9oUToICrB0Sa10FOW35C0'
end
it 'renders code' do
is_expected.to include 'language-yaml'
is_expected.to include '<span class="na">test</span>'
is_expected.to include '<span class="kc">true</span>'
end
end
end

View File

@ -88,7 +88,7 @@ RSpec.describe ClickHouse::Iterator, :click_house, feature_category: :database d
end
it 'returns no data' do
expect(collect_ids_with_batch_size(3)).to match_array([])
expect(collect_ids_with_batch_size(3)).to be_empty
end
end
end

View File

@ -46,33 +46,6 @@ RSpec.describe Gitlab::ApplicationSettingFetcher, feature_category: :cell do
context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is false' do
let_it_be(:settings) { create(:application_setting) }
context 'and an error is raised' do
before do
# The cached method is called twice:
# - ApplicationSettingFetcher
# - ApplicationSetting (CachedAttribute module)
# For this test, the first needs to raise an exception
# The second is swallowed on production so that should not raise an exception
# So we only let the first call raise an exception
# Alternatively, we could mock Rails.env.production? but I prefer not to
raise_exception = true
allow(ApplicationSetting).to receive(:cached).twice do
if raise_exception
raise_exception = false
raise(StandardError)
else
ApplicationSetting.last
end
end
end
it 'will retrieve uncached ApplicationSetting' do
expect(ApplicationSetting).to receive(:current).and_call_original
expect(current_application_settings).to eq(settings)
end
end
context 'and settings in cache' do
before do
# Warm the cache

View File

@ -5,6 +5,7 @@ require 'nokogiri'
module Gitlab
RSpec.describe Asciidoc, feature_category: :wiki do
include RepoHelpers
include FakeBlobHelpers
before do
@ -1004,6 +1005,63 @@ module Gitlab
end
end
context 'when using include in code segements' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:ref) { 'markdown' }
let_it_be(:requested_path) { '/' }
let_it_be(:commit) { project.commit(ref) }
let_it_be(:context) do
{
commit: commit,
project: project,
ref: ref,
text_source: :blob,
requested_path: requested_path,
no_sourcepos: true
}
end
let_it_be(:project_files) do
{
'diagram.puml' => "@startuml\nBob -> Sara : Hello\n@enduml",
'code.yaml' => "---\ntest: true"
}
end
let(:input) do
<<~ADOC
[plantuml]
----
include::diagram.puml[]
----
[,yaml]
----
include::code.yaml[]
----
ADOC
end
subject(:output) { render(input, context) }
around do |example|
create_and_delete_files(project, project_files, branch_name: ref) do
example.run
end
end
it 'renders PlanUML' do
stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
is_expected.to include 'http://localhost:8080/png/U9npA2v9B2efpStXSifFKj2rKmXEB4fKi5BmICt9oUToICrB0Se10EdD34a0'
end
it 'renders code' do
is_expected.to include 'language-yaml'
is_expected.to include '<span class="na">test</span>'
is_expected.to include '<span class="kc">true</span>'
end
end
it 'detects and converts to a wikilink' do
tag = '[[text|url]]'
html = render("See #{tag}", {})

View File

@ -63,7 +63,7 @@ RSpec.describe Gitlab::Checks::ChangesAccess, feature_category: :source_code_man
it 'calls #new_commits' do
expect(project.repository).to receive(:new_commits).and_call_original
expect(subject.commits).to match_array([])
expect(subject.commits).to be_empty
end
context 'when change is for notes ref' do
@ -72,7 +72,7 @@ RSpec.describe Gitlab::Checks::ChangesAccess, feature_category: :source_code_man
end
it 'does not return any commits' do
expect(subject.commits).to match_array([])
expect(subject.commits).to be_empty
end
end

View File

@ -34,7 +34,7 @@ RSpec.describe Gitlab::Ci::Jwt, feature_category: :secrets_management do
it 'has correct values for the custom attributes' do
aggregate_failures do
expect(payload[:groups_direct]).to match_array([])
expect(payload[:groups_direct]).to be_empty
expect(payload[:namespace_id]).to eq(namespace.id.to_s)
expect(payload[:namespace_path]).to eq(namespace.full_path)
expect(payload[:project_id]).to eq(project.id.to_s)

View File

@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Release do
it 'returns without error' do
builder = subject
expect(builder).to match_array([])
expect(builder.to_a).to be_empty
expect(builder.errors).to be_nil
end
end

View File

@ -93,6 +93,7 @@ work_item_type:
- parent_restrictions
- allowed_child_types_by_name
- allowed_parent_types_by_name
- user_preferences
events:
- author
- project

View File

@ -163,7 +163,7 @@ RSpec.describe Gitlab::MailRoom, feature_category: :build do
let(:custom_config) { { enabled: false } }
it 'returns an empty array' do
expect(described_class.enabled_mailbox_types).to match_array([])
expect(described_class.enabled_mailbox_types).to be_empty
end
end
end

View File

@ -104,7 +104,7 @@ RSpec.describe Gitlab::SearchResults, feature_category: :global_search do
with_them do
it 'returns an empty array' do
expect(results.aggregations(scope)).to match_array([])
expect(results.aggregations(scope)).to be_empty
end
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe ::Search::EmptySearchResults, feature_category: :global_search do
describe '#objects' do
it 'returns an empty array' do
expect(results.objects).to match_array([])
expect(results.objects).to be_empty
end
end
@ -37,7 +37,7 @@ RSpec.describe ::Search::EmptySearchResults, feature_category: :global_search do
describe '#aggregations' do
it 'returns an empty array' do
expect(results.aggregations).to match_array([])
expect(results.aggregations).to be_empty
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::Types::UserPreference, type: :model, feature_category: :team_planning do
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:namespace) }
it { is_expected.to belong_to(:work_item_type).class_name('WorkItems::Type').inverse_of(:user_preferences) }
end
end

View File

@ -70,6 +70,7 @@ RSpec.describe 'Query.project.pipelineAnalytics', :aggregate_failures, :click_ho
let(:fields) do
<<~QUERY
aggregate { #{period_fields} }
timeSeries(period: DAY) { #{period_fields} }
weekPipelinesTotals
weekPipelinesLabels

View File

@ -34,12 +34,12 @@ RSpec.describe ::Ci::CollectAggregatePipelineAnalyticsService, :click_house, :en
using RSpec::Parameterized::TableSyntax
where(:status_groups, :duration_percentiles, :expected_aggregate) do
%i[any] | [] | { count: { any: 7 } }
%i[any] | [50, 75] | { count: { any: 7 }, duration_statistics: { p50: 30.minutes, p75: 82.5.minutes } }
%i[any success] | [] | { count: { any: 7, success: 2 } }
%i[success other] | [] | { count: { success: 2, other: 2 } }
%i[any] | [] | { count: { any: 8 } }
%i[any] | [50, 75] | { count: { any: 8 }, duration_statistics: { p50: 30.minutes, p75: 63.75.minutes } }
%i[any success] | [] | { count: { any: 8, success: 2 } }
%i[success other] | [] | { count: { success: 2, other: 3 } }
%i[failed] | [50, 75] |
{ count: { failed: 2 }, duration_statistics: { p50: 30.minutes, p75: 82.5.minutes } }
{ count: { failed: 2 }, duration_statistics: { p50: 30.minutes, p75: 63.75.minutes } }
end
with_them do
@ -60,7 +60,7 @@ RSpec.describe ::Ci::CollectAggregatePipelineAnalyticsService, :click_house, :en
expect(result).to be_success
expect(result.errors).to eq([])
expect(result.payload[:aggregate]).to eq(
count: { any: 7 }, duration_statistics: { p50: 30.minutes, p99: 67.8.hours }
count: { any: 8 }, duration_statistics: { p50: 30.minutes, p99: 4026.minutes }
)
end
end
@ -81,7 +81,7 @@ RSpec.describe ::Ci::CollectAggregatePipelineAnalyticsService, :click_house, :en
it 'does not include job starting 1 second before start of week' do
expect(result).to be_success
expect(result.errors).to eq([])
expect(result.payload[:aggregate]).to eq(count: { any: 7 })
expect(result.payload[:aggregate]).to eq(count: { any: 8 })
end
end
@ -92,7 +92,7 @@ RSpec.describe ::Ci::CollectAggregatePipelineAnalyticsService, :click_house, :en
it 'includes job starting 1 second before start of week' do
expect(result).to be_success
expect(result.errors).to eq([])
expect(result.payload[:aggregate]).to eq(count: { any: 8 })
expect(result.payload[:aggregate]).to eq(count: { any: 9 })
end
end

View File

@ -17,7 +17,7 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
let(:expected_1st_day) { (from_time || 1.week.ago.utc).beginning_of_day }
let(:time_series_period) { :day }
let(:time_series_filters) do
{ duration_statistics: duration_percentile_symbols }
{ count: status_groups, duration_statistics: duration_percentile_symbols }
end
let(:service) do
@ -53,6 +53,7 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
let(:no_pipeline_statistics) do
{
count: { success: 0, failed: 0, other: 0, any: 0 },
duration_statistics: duration_percentile_symbols.index_with(0.seconds)
}
end
@ -61,21 +62,25 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
[
{
label: Time.utc(2023, 1, 1),
count: { success: 0, failed: 1, other: 0, any: 1 },
duration_statistics: { p50: 45.minutes, p95: 45.minutes, p99: 45.minutes }
},
{ label: Time.utc(2023, 1, 2), **no_pipeline_statistics },
{
label: Time.utc(2023, 1, 3),
duration_statistics: { p50: 3600.5.seconds, p95: 6840.05.seconds, p99: 7128.01.seconds }
count: { success: 0, failed: 1, other: 2, any: 3 },
duration_statistics: { p50: 60.seconds, p95: 6486.seconds, p99: 7057.2.seconds }
},
{ label: Time.utc(2023, 1, 4), **no_pipeline_statistics },
{ label: Time.utc(2023, 1, 5), **no_pipeline_statistics },
{
label: Time.utc(2023, 1, 6),
count: { success: 1, failed: 0, other: 0, any: 1 },
duration_statistics: { p50: 3.days, p95: 3.days, p99: 3.days }
},
{
label: Time.utc(2023, 1, 7),
count: { success: 1, failed: 0, other: 1, any: 3 },
duration_statistics: { p50: 30.minutes, p95: 30.minutes, p99: 30.minutes }
}
]
@ -87,6 +92,7 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
%i[any] | []
%i[any] | [50, 95]
%i[success other] | []
%i[failed other] | [50, 99]
%i[failed] | [50, 95]
end
@ -128,8 +134,13 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
expect(result).to be_success
expect(result.errors).to eq([])
expect(result.payload[:time_series]).to eq([
{ label: Time.utc(2022, 12, 26), duration_statistics: { p50: 45.minutes, p95: 45.minutes } },
{ label: Time.utc(2023, 1, 2), duration_statistics: { p50: 30.minutes, p95: 54.5.hours } }
{
label: Time.utc(2022, 12, 26), count: { any: 1 },
duration_statistics: { p50: 45.minutes, p95: 45.minutes }
},
{
label: Time.utc(2023, 1, 2), count: { any: 7 }, duration_statistics: { p50: 30.minutes, p95: 51.hours }
}
])
end
end
@ -143,7 +154,9 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
expect(result).to be_success
expect(result.errors).to eq([])
expect(result.payload[:time_series]).to eq([
{ label: Time.utc(2023, 1, 1), duration_statistics: { p50: 30.minutes, p95: 51.hours } }
{
label: Time.utc(2023, 1, 1), count: { any: 8 }, duration_statistics: { p50: 30.minutes, p95: 47.5.hours }
}
])
end
end
@ -194,7 +207,10 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
it 'includes job starting 1 second before start of week' do
expect(result).to be_success
expect(result.errors).to eq([])
expect(result.payload[:time_series]).to eq([{ label: Time.utc(2022, 12, 31) }, *expected_time_series])
expect(result.payload[:time_series]).to eq([
{ label: Time.utc(2022, 12, 31), count: { any: 1 } },
*expected_time_series
])
end
end
@ -223,7 +239,7 @@ RSpec.describe ::Ci::CollectTimeSeriesPipelineAnalyticsService, :click_house, :e
expect(result).to be_success
expect(result.errors).to eq([])
expect(result.payload[:time_series]).to eq([
{ label: Time.utc(2023, 1, 1) },
{ label: Time.utc(2023, 1, 1), count: { any: 3, failed: 1, success: 2 } },
*no_pipeline_statistics_for_day_range(2..7)
])
end

View File

@ -17,6 +17,7 @@ RSpec.shared_context 'with pipelines executed on different projects' do
create_pipeline(project1, :success, 1.5.days.before(ending_time), 3.days), # 2023-01-06
create_pipeline(project1, :failed, 5.days.before(ending_time), 2.hours), # 2023-01-03
create_pipeline(project1, :skipped, 5.days.before(ending_time), 1.second), # 2023-01-03
create_pipeline(project1, :canceled, 5.days.before(ending_time), 1.minute), # 2023-01-03
create_pipeline(project1, :failed, 1.week.before(ending_time), 45.minutes), # 2023-01-01
create_pipeline(project1, :skipped, 1.second.before(starting_time), 45.minutes) # 2022-12-31
]

View File

@ -37,10 +37,23 @@ RSpec.shared_examples_for 'a pipeline analytics service' do
include_examples 'returns Not allowed error'
end
context 'when ClickHouse query raises error' do
before do
allow(::ClickHouse::Client).to receive(:select).with(anything, :main)
.and_raise(::ClickHouse::Client::DatabaseError, 'some error')
end
it 'returns error response', :aggregate_failures do
expect { result }.not_to raise_error
expect(result.error?).to be true
expect(result.errors).to contain_exactly('some error')
end
end
context 'when project is not specified' do
let(:project) { nil }
it 'returns error' do
it 'returns error response', :aggregate_failures do
expect(result.error?).to be true
expect(result.errors).to contain_exactly('Project must be specified')
end
@ -49,7 +62,7 @@ RSpec.shared_examples_for 'a pipeline analytics service' do
context 'when invalid duration percentiles are specified' do
let(:duration_percentiles) { [50, 70, 90] }
it 'returns error', :aggregate_failures do
it 'returns error response', :aggregate_failures do
expect(result.error?).to be true
expect(result.message).to eq 'Invalid duration percentiles specified'
end