Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-24 12:12:57 +00:00
parent 563c8efdee
commit e40c68997d
57 changed files with 678 additions and 340 deletions

View File

@ -311,23 +311,6 @@ Performance/StringInclude:
- 'spec/spec_helper.rb'
- 'spec/support_specs/helpers/active_record/query_recorder_spec.rb'
# Offense count: 18
# Cop supports --auto-correct.
Performance/Sum:
Exclude:
- 'app/controllers/projects/pipelines/tests_controller.rb'
- 'app/models/application_setting_implementation.rb'
- 'app/models/ci/pipeline.rb'
- 'app/services/issues/export_csv_service.rb'
- 'ee/spec/lib/gitlab/elastic/bulk_indexer_spec.rb'
- 'lib/api/entities/issuable_time_stats.rb'
- 'lib/container_registry/tag.rb'
- 'lib/gitlab/ci/reports/test_suite_comparer.rb'
- 'lib/gitlab/diff/file.rb'
- 'lib/gitlab/usage_data.rb'
- 'lib/peek/views/detailed_view.rb'
- 'spec/models/namespace/root_storage_statistics_spec.rb'
# Offense count: 15209
# Configuration parameters: Prefixes.
# Prefixes: when, with, without

View File

@ -1,5 +1,4 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlBarChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { chartHeight } from '../../constants';
@ -9,9 +8,6 @@ export default {
components: {
GlBarChart,
},
directives: {
GlResizeObserverDirective,
},
props: {
graphData: {
type: Object,
@ -60,11 +56,6 @@ export default {
formatLegendLabel(query) {
return query.label;
},
onResize() {
if (!this.$refs.barChart) return;
const { width } = this.$refs.barChart.$el.getBoundingClientRect();
this.width = width;
},
setSvg(name) {
getSvgIconPathContent(name)
.then((path) => {
@ -81,17 +72,16 @@ export default {
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize">
<gl-bar-chart
ref="barChart"
v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:width="width"
:height="height"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
:x-axis-type="xAxisType"
/>
</div>
<gl-bar-chart
ref="barChart"
v-bind="$attrs"
:responsive="true"
:data="chartData"
:option="chartOptions"
:width="width"
:height="height"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
:x-axis-type="xAxisType"
/>
</template>

View File

@ -1,5 +1,4 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
@ -12,9 +11,6 @@ export default {
components: {
GlColumnChart,
},
directives: {
GlResizeObserverDirective,
},
props: {
graphData: {
type: Object,
@ -83,11 +79,6 @@ export default {
formatLegendLabel(query) {
return query.label;
},
onResize() {
if (!this.$refs.columnChart) return;
const { width } = this.$refs.columnChart.$el.getBoundingClientRect();
this.width = width;
},
setSvg(name) {
getSvgIconPathContent(name)
.then((path) => {
@ -101,17 +92,16 @@ export default {
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize">
<gl-column-chart
ref="columnChart"
v-bind="$attrs"
:bars="barChartData"
:option="chartOptions"
:width="width"
:height="height"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
:x-axis-type="xAxisType"
/>
</div>
<gl-column-chart
ref="columnChart"
v-bind="$attrs"
:responsive="true"
:bars="barChartData"
:option="chartOptions"
:width="width"
:height="height"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
:x-axis-type="xAxisType"
/>
</template>

View File

@ -1,5 +1,4 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlGaugeChart } from '@gitlab/ui/dist/charts';
import { isFinite, isArray, isInteger } from 'lodash';
import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
@ -10,9 +9,6 @@ export default {
components: {
GlGaugeChart,
},
directives: {
GlResizeObserverDirective,
},
props: {
graphData: {
type: Object,
@ -96,27 +92,19 @@ export default {
return this.queryResult || NaN;
},
},
methods: {
onResize() {
if (!this.$refs.gaugeChart) return;
const { width } = this.$refs.gaugeChart.$el.getBoundingClientRect();
this.width = width;
},
},
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize">
<gl-gauge-chart
ref="gaugeChart"
v-bind="$attrs"
:value="value"
:min="rangeValues.min"
:max="rangeValues.max"
:thresholds="thresholdsValue"
:text="textValue"
:split-number="splitValue"
:width="width"
/>
</div>
<gl-gauge-chart
ref="gaugeChart"
v-bind="$attrs"
:responsive="true"
:value="value"
:min="rangeValues.min"
:max="rangeValues.max"
:thresholds="thresholdsValue"
:text="textValue"
:split-number="splitValue"
:width="width"
/>
</template>

View File

@ -1,5 +1,4 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlHeatmap } from '@gitlab/ui/dist/charts';
import { formatDate, timezones, formats } from '../../format_date';
import { graphDataValidatorForValues } from '../../utils';
@ -8,9 +7,6 @@ export default {
components: {
GlHeatmap,
},
directives: {
GlResizeObserverDirective,
},
props: {
graphData: {
type: Object,
@ -61,26 +57,18 @@ export default {
return this.graphData.metrics[0];
},
},
methods: {
onResize() {
if (this.$refs.heatmapChart) return;
const { width } = this.$refs.heatmapChart.$el.getBoundingClientRect();
this.width = width;
},
},
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize">
<gl-heatmap
ref="heatmapChart"
v-bind="$attrs"
:data-series="chartData"
:x-axis-name="xAxisName"
:y-axis-name="yAxisName"
:x-axis-labels="xAxisLabels"
:y-axis-labels="yAxisLabels"
:width="width"
/>
</div>
<gl-heatmap
ref="heatmapChart"
v-bind="$attrs"
:responsive="true"
:data-series="chartData"
:x-axis-name="xAxisName"
:y-axis-name="yAxisName"
:x-axis-labels="xAxisLabels"
:y-axis-labels="yAxisLabels"
:width="width"
/>
</template>

View File

@ -1,5 +1,4 @@
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlStackedColumnChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { s__ } from '~/locale';
@ -12,9 +11,6 @@ export default {
components: {
GlStackedColumnChart,
},
directives: {
GlResizeObserverDirective,
},
props: {
graphData: {
type: Object,
@ -125,32 +121,26 @@ export default {
console.error('SVG could not be rendered correctly: ', e);
});
},
onResize() {
if (!this.$refs.chart) return;
const { width } = this.$refs.chart.$el.getBoundingClientRect();
this.width = width;
},
},
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize">
<gl-stacked-column-chart
ref="chart"
v-bind="$attrs"
:bars="chartData"
:option="chartOptions"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
:x-axis-type="xAxisType"
:group-by="groupBy"
:width="width"
:height="height"
:legend-layout="legendLayout"
:legend-average-text="legendAverageText"
:legend-current-text="legendCurrentText"
:legend-max-text="legendMaxText"
:legend-min-text="legendMinText"
/>
</div>
<gl-stacked-column-chart
ref="chart"
v-bind="$attrs"
:responsive="true"
:bars="chartData"
:option="chartOptions"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
:x-axis-type="xAxisType"
:group-by="groupBy"
:width="width"
:height="height"
:legend-layout="legendLayout"
:legend-average-text="legendAverageText"
:legend-current-text="legendCurrentText"
:legend-max-text="legendMaxText"
:legend-min-text="legendMinText"
/>
</template>

View File

@ -10,6 +10,7 @@ import {
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
} from '~/security_configuration/constants';
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/utils/optimistic_response';
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql';
@ -51,7 +52,6 @@ export default {
data() {
return {
errorMessage: '',
providerLoadingId: null,
securityTrainingProviders: [],
hasTouchedConfiguration: false,
};
@ -99,8 +99,6 @@ export default {
this.storeProvider({ ...provider, isEnabled: toggledIsEnabled });
},
async storeProvider({ id, isEnabled, isPrimary }) {
this.providerLoadingId = id;
try {
const {
data: {
@ -116,6 +114,11 @@ export default {
isPrimary,
},
},
optimisticResponse: updateSecurityTrainingOptimisticResponse({
id,
isEnabled,
isPrimary,
}),
});
if (errors.length > 0) {
@ -126,8 +129,6 @@ export default {
this.hasTouchedConfiguration = true;
} catch {
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
} finally {
this.providerLoadingId = null;
}
},
trackProviderToggle(providerId, providerIsEnabled) {
@ -173,7 +174,6 @@ export default {
:value="provider.isEnabled"
:label="__('Training mode')"
label-position="hidden"
:is-loading="providerLoadingId === provider.id"
@change="toggleProvider(provider)"
/>
<div class="gl-ml-5">

View File

@ -0,0 +1,13 @@
export const updateSecurityTrainingOptimisticResponse = (changes) => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
securityTrainingUpdate: {
__typename: 'SecurityTrainingUpdatePayload',
training: {
__typename: 'ProjectSecurityTraining',
...changes,
},
errors: [],
},
});

View File

@ -8,6 +8,7 @@ import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
const KEY_EDIT = 'edit';
const KEY_WEB_IDE = 'webide';
const KEY_GITPOD = 'gitpod';
const KEY_PIPELINE_EDITOR = 'pipeline_editor';
export default {
components: {
@ -64,6 +65,11 @@ export default {
required: false,
default: false,
},
showPipelineEditorButton: {
type: Boolean,
required: false,
default: false,
},
userPreferencesGitpodPath: {
type: String,
required: false,
@ -79,6 +85,11 @@ export default {
required: false,
default: '',
},
pipelineEditorUrl: {
type: String,
required: false,
default: '',
},
webIdeUrl: {
type: String,
required: false,
@ -117,14 +128,19 @@ export default {
},
data() {
return {
selection: KEY_WEB_IDE,
selection: this.showPipelineEditorButton ? KEY_PIPELINE_EDITOR : KEY_WEB_IDE,
showEnableGitpodModal: false,
showForkModal: false,
};
},
computed: {
actions() {
return [this.webIdeAction, this.editAction, this.gitpodAction].filter((action) => action);
return [
this.pipelineEditorAction,
this.webIdeAction,
this.editAction,
this.gitpodAction,
].filter((action) => action);
},
editAction() {
if (!this.showEditButton) {
@ -209,6 +225,24 @@ export default {
this.showGitpodButton && this.userPreferencesGitpodPath && this.userProfileEnableGitpodPath
);
},
pipelineEditorAction() {
if (!this.showPipelineEditorButton) {
return null;
}
const secondaryText = __('Edit, lint, and visualize your pipeline.');
return {
key: KEY_PIPELINE_EDITOR,
text: __('Edit in pipeline editor'),
secondaryText,
tooltip: secondaryText,
attrs: {
'data-qa-selector': 'pipeline_editor_button',
},
href: this.pipelineEditorUrl,
};
},
gitpodAction() {
if (!this.computedShowGitpodButton) {
return null;

View File

@ -42,9 +42,9 @@ module Projects
end
def test_suite
suite = builds.map do |build|
suite = builds.sum do |build|
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
end.sum
end
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, project).load!

View File

@ -345,11 +345,7 @@ class ProjectsController < Projects::ApplicationController
private
def refs_params
if Feature.enabled?(:strong_parameters_for_project_controller, @project, default_enabled: :yaml)
params.permit(:search, :sort, :ref, find: [])
else
params
end
params.permit(:search, :sort, :ref, find: [])
end
# Render project landing depending of which features are available

View File

@ -7,21 +7,21 @@ module Types
authorize :read_release
field :external, GraphQL::Types::Boolean, null: true, method: :external?,
description: 'Indicates the link points to an external resource.'
field :id, GraphQL::Types::ID, null: false,
description: 'ID of the link.'
field :link_type, Types::ReleaseAssetLinkTypeEnum, null: true,
description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`.'
field :name, GraphQL::Types::String, null: true,
description: 'Name of the link.'
field :url, GraphQL::Types::String, null: true,
description: 'URL of the link.'
field :link_type, Types::ReleaseAssetLinkTypeEnum, null: true,
description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`.'
field :external, GraphQL::Types::Boolean, null: true, method: :external?,
description: 'Indicates the link points to an external resource.'
field :direct_asset_url, GraphQL::Types::String, null: true,
description: 'Direct asset URL of the link.'
field :direct_asset_path, GraphQL::Types::String, null: true, method: :filepath,
description: 'Relative path for the direct asset link.'
field :direct_asset_url, GraphQL::Types::String, null: true,
description: 'Direct asset URL of the link.'
def direct_asset_url
return object.url unless object.filepath

View File

@ -10,25 +10,25 @@ module Types
present_using ReleasePresenter
field :self_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the release.'
field :edit_url, GraphQL::Types::String, null: true,
description: "HTTP URL of the release's edit page.",
authorize: :update_release
field :opened_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.',
authorize: :download_code
field :merged_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.',
field :closed_issues_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.',
authorize: :download_code
field :closed_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.',
authorize: :download_code
field :edit_url, GraphQL::Types::String, null: true,
description: "HTTP URL of the release's edit page.",
authorize: :update_release
field :merged_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.',
authorize: :download_code
field :opened_issues_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=open`.',
authorize: :download_code
field :closed_issues_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.',
field :opened_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.',
authorize: :download_code
field :self_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the release.'
end
end

View File

@ -13,30 +13,30 @@ module Types
present_using ReleasePresenter
field :tag_name, GraphQL::Types::String, null: true, method: :tag,
description: 'Name of the tag associated with the release.'
field :tag_path, GraphQL::Types::String, null: true,
description: 'Relative web path to the tag associated with the release.',
authorize: :download_code
field :description, GraphQL::Types::String, null: true,
description: 'Description (also known as "release notes") of the release.'
field :name, GraphQL::Types::String, null: true,
description: 'Name of the release.'
field :created_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was created.'
field :released_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was released.'
field :upcoming_release, GraphQL::Types::Boolean, null: true, method: :upcoming_release?,
description: 'Indicates the release is an upcoming release.'
field :assets, Types::ReleaseAssetsType, null: true, method: :itself,
description: 'Assets of the release.'
field :created_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was created.'
field :description, GraphQL::Types::String, null: true,
description: 'Description (also known as "release notes") of the release.'
field :evidences, Types::EvidenceType.connection_type, null: true,
description: 'Evidence for the release.'
field :links, Types::ReleaseLinksType, null: true, method: :itself,
description: 'Links of the release.'
field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones associated to the release.',
resolver: ::Resolvers::ReleaseMilestonesResolver
field :evidences, Types::EvidenceType.connection_type, null: true,
description: 'Evidence for the release.'
field :name, GraphQL::Types::String, null: true,
description: 'Name of the release.'
field :released_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was released.'
field :tag_name, GraphQL::Types::String, null: true, method: :tag,
description: 'Name of the tag associated with the release.'
field :tag_path, GraphQL::Types::String, null: true,
description: 'Relative web path to the tag associated with the release.',
authorize: :download_code
field :upcoming_release, GraphQL::Types::Boolean, null: true, method: :upcoming_release?,
description: 'Indicates the release is an upcoming release.'
field :author, Types::UserType, null: true,
description: 'User that created the release.'

View File

@ -6,17 +6,6 @@ module Types
authorize :download_code
field :root_ref, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default branch of the repository.'
field :empty, GraphQL::Types::Boolean, null: false, method: :empty?, calls_gitaly: true,
description: 'Indicates repository has no visible content.'
field :exists, GraphQL::Types::Boolean, null: false, method: :exists?, calls_gitaly: true,
description: 'Indicates a corresponding Git repository exists on disk.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.'
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
max_page_size: 100,
description: 'Paginated tree of the repository.'
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository'
field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true,
@ -26,5 +15,16 @@ module Types
description: 'Shows a disk path of the repository.',
null: true,
authorize: :read_storage_disk_path
field :empty, GraphQL::Types::Boolean, null: false, method: :empty?, calls_gitaly: true,
description: 'Indicates repository has no visible content.'
field :exists, GraphQL::Types::Boolean, null: false, method: :exists?, calls_gitaly: true,
description: 'Indicates a corresponding Git repository exists on disk.'
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
max_page_size: 100,
description: 'Paginated tree of the repository.'
field :root_ref, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default branch of the repository.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.'
end
end

View File

@ -6,15 +6,15 @@ module Types
authorize :read_statistics
field :storage_size, GraphQL::Types::Float, null: false, description: 'Total storage in bytes.'
field :repository_size, GraphQL::Types::Float, null: false, description: 'Git repository size in bytes.'
field :lfs_objects_size, GraphQL::Types::Float, null: false, description: 'LFS objects size in bytes.'
field :build_artifacts_size, GraphQL::Types::Float, null: false, description: 'CI artifacts size in bytes.'
field :packages_size, GraphQL::Types::Float, null: false, description: 'Packages size in bytes.'
field :wiki_size, GraphQL::Types::Float, null: false, description: 'Wiki size in bytes.'
field :snippets_size, GraphQL::Types::Float, null: false, description: 'Snippets size in bytes.'
field :pipeline_artifacts_size, GraphQL::Types::Float, null: false, description: 'CI pipeline artifacts size in bytes.'
field :uploads_size, GraphQL::Types::Float, null: false, description: 'Uploads size in bytes.'
field :dependency_proxy_size, GraphQL::Types::Float, null: false, description: 'Dependency Proxy sizes in bytes.'
field :lfs_objects_size, GraphQL::Types::Float, null: false, description: 'LFS objects size in bytes.'
field :packages_size, GraphQL::Types::Float, null: false, description: 'Packages size in bytes.'
field :pipeline_artifacts_size, GraphQL::Types::Float, null: false, description: 'CI pipeline artifacts size in bytes.'
field :repository_size, GraphQL::Types::Float, null: false, description: 'Git repository size in bytes.'
field :snippets_size, GraphQL::Types::Float, null: false, description: 'Snippets size in bytes.'
field :storage_size, GraphQL::Types::Float, null: false, description: 'Total storage in bytes.'
field :uploads_size, GraphQL::Types::Float, null: false, description: 'Uploads size in bytes.'
field :wiki_size, GraphQL::Types::Float, null: false, description: 'Wiki size in bytes.'
end
end

View File

@ -8,10 +8,10 @@ module Types
graphql_name 'TaskCompletionStatus'
description 'Completion status of tasks'
field :count, GraphQL::Types::Int, null: false,
description: 'Number of total tasks.'
field :completed_count, GraphQL::Types::Int, null: false,
description: 'Number of completed tasks.'
field :count, GraphQL::Types::Int, null: false,
description: 'Number of total tasks.'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -9,15 +9,15 @@ module Types
implements Types::Tree::EntryType
present_using BlobPresenter
field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the blob.'
field :web_path, GraphQL::Types::String, null: true,
description: 'Web path of the blob.'
field :lfs_oid, GraphQL::Types::String, null: true,
calls_gitaly: true,
description: 'LFS ID of the blob.'
field :mode, GraphQL::Types::String, null: true,
description: 'Blob mode in numeric format.'
field :web_path, GraphQL::Types::String, null: true,
description: 'Web path of the blob.'
field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the blob.'
def lfs_oid
Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find

View File

@ -203,9 +203,11 @@ module TreeHelper
show_edit_button: show_edit_button?(options),
show_web_ide_button: show_web_ide_button?,
show_gitpod_button: show_gitpod_button?,
show_pipeline_editor_button: show_pipeline_editor_button?(@project, @path),
web_ide_url: web_ide_url,
edit_url: edit_url(options),
pipeline_editor_url: project_ci_pipeline_editor_path(@project, branch_name: @ref),
gitpod_url: gitpod_url,
user_preferences_gitpod_path: profile_preferences_path(anchor: 'user_gitpod_enabled'),

View File

@ -29,6 +29,10 @@ module WebIdeButtonHelper
show_web_ide_button? && Gitlab::CurrentSettings.gitpod_enabled
end
def show_pipeline_editor_button?(project, path)
can_view_pipeline_editor?(project) && path == project.ci_config_path_or_default
end
def can_push_code?
current_user&.can?(:push_code, @project)
end

View File

@ -403,7 +403,7 @@ module ApplicationSettingImplementation
def normalized_repository_storage_weights
strong_memoize(:normalized_repository_storage_weights) do
repository_storages_weights = repository_storages_weighted.slice(*Gitlab.config.repositories.storages.keys)
weights_total = repository_storages_weights.values.reduce(:+)
weights_total = repository_storages_weights.values.sum
repository_storages_weights.transform_values do |w|
next w if weights_total == 0

View File

@ -10,6 +10,8 @@ module Ci
include Presentable
include Importable
include Ci::HasRef
include HasDeploymentName
extend ::Gitlab::Utils::Override
BuildArchivedError = Class.new(StandardError)
@ -35,6 +37,8 @@ module Ci
DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
RUNNERS_STATUS_CACHE_EXPIRATION = 1.minute
DEPLOYMENT_NAMES = %w[deploy release rollout].freeze
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id
@ -1123,6 +1127,10 @@ module Ci
.include?(exit_code)
end
def track_deployment_usage
Gitlab::Utils::UsageData.track_usage_event('ci_users_executing_deployment_job', user_id) if user_id.present? && count_user_deployment?
end
protected
def run_status_commit_hooks!

View File

@ -653,7 +653,7 @@ module Ci
def coverage
coverage_array = latest_statuses.map(&:coverage).compact
if coverage_array.size >= 1
coverage_array.reduce(:+) / coverage_array.size
coverage_array.sum / coverage_array.size
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Ci
module HasDeploymentName
extend ActiveSupport::Concern
def count_user_deployment?
Feature.enabled?(:job_deployment_count) && deployment_name?
end
def deployment_name?
self.class::DEPLOYMENT_NAMES.any? { |n| name.downcase.include?(n) }
end
end
end

View File

@ -52,7 +52,7 @@ module Issues
# rubocop: disable CodeReuse/ActiveRecord
def issue_time_spent(issue)
issue.timelogs.map(&:time_spent).sum
issue.timelogs.sum(&:time_spent)
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -1,11 +1,10 @@
- f ||= local_assigns[:f]
.project-templates-buttons
%ul.nav-tabs.nav-links.nav.scrolling-tabs
%li.built-in-tab
%a.nav-link.active{ href: "#built-in", data: { toggle: 'tab'} }
= _('Built-in')
= gl_tab_counter_badge Gitlab::ProjectTemplate.all.count + Gitlab::SampleDataTemplate.all.count
= gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
= gl_tab_link_to '#built-in', tab_class: 'built-in-tab', class: 'active', data: { toggle: 'tab' } do
= _('Built-in')
= gl_tab_counter_badge Gitlab::ProjectTemplate.all.count + Gitlab::SampleDataTemplate.all.count
.tab-content
.project-templates-buttons.import-buttons.tab-pane.active#built-in

View File

@ -6,7 +6,7 @@
.file-actions.gl-display-flex.gl-align-items-center.gl-flex-wrap.gl-md-justify-content-end<
= render 'projects/blob/viewer_switcher', blob: blob unless blame
= render 'shared/web_ide_button', blob: blob
- if can_view_pipeline_editor?(@project) && @path == @project.ci_config_path_or_default
- if show_pipeline_editor_button?(@project, @path)
= link_to "Pipeline Editor", project_ci_pipeline_editor_path(@project, branch_name: @ref), class: "btn gl-button btn-confirm-secondary gl-ml-3"
.btn-group{ role: "group", class: ("gl-ml-3" if current_user) }>
= render_if_exists 'projects/blob/header_file_locks_link'

View File

@ -39,6 +39,7 @@ module Ci
# We execute these async as these are independent operations.
BuildHooksWorker.perform_async(build.id)
ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat?
build.track_deployment_usage
if build.failed? && !build.auto_retry_expected?
::Ci::MergeRequests::AddTodoWhenBuildFailsWorker.perform_async(build.id)

View File

@ -1,8 +1,8 @@
---
name: strong_parameters_for_project_controller
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79956
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352251
name: job_deployment_count
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79272
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351591
milestone: '14.8'
type: development
group: group::source code
group: group::release
default_enabled: false

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.ci_users.ci_users_executing_deployment_job_monthly
description: Monthly counts of times users have executed deployment jobs
product_section: ops
product_stage: release
product_group: group::release
product_category: continuous_delivery
value_type: number
status: active
milestone: "14.8"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79272
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
options:
events:
- ci_users_executing_deployment_job
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.ci_users.ci_users_executing_deployment_job_weekly
description: Weekly counts of times users have executed deployment jobs
product_section: ops
product_stage: release
product_group: group::release
product_category: continuous_delivery
value_type: number
status: active
milestone: "14.8"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79272
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
options:
events:
- ci_users_executing_deployment_job
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -4,13 +4,13 @@ group: Release
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# CI/CD Analytics **(FREE)**
# CI/CD analytics **(FREE)**
## Pipeline success and duration charts
> [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/38318) to CI/CD Analytics in GitLab 12.8.
CI/CD Analytics shows the history of your pipeline successes and failures, as well as how long each pipeline
CI/CD analytics shows the history of your pipeline successes and failures, as well as how long each pipeline
ran.
View successful pipelines:
@ -39,21 +39,14 @@ To view CI/CD analytics:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.7.
> - [Added support](https://gitlab.com/gitlab-org/gitlab/-/issues/291746) for lead time for changes in GitLab 13.10.
Customer experience is a key metric. Users want to measure platform stability and other
post-deployment performance KPIs, and set targets for customer behavior, experience, and financial
impact. Tracking and measuring these indicators solves an important pain point. Similarly, creating
views that manage products, not projects or repositories, provides users with a more relevant data set.
Since GitLab is a tool for the entire DevOps life-cycle, information from different workflows is
integrated and can be used to measure the success of the teams.
The DevOps Research and Assessment ([DORA](https://cloud.google.com/blog/products/devops-sre/the-2019-accelerate-state-of-devops-elite-performance-productivity-and-scaling))
team developed four key metrics that the industry has widely adopted. You can use these metrics as
performance indicators for software development teams:
team developed several key metrics that you can use as performance indicators for software development
teams:
- Deployment frequency: How often an organization successfully releases to production.
- Lead time for changes: The amount of time it takes for code to reach production.
- Change failure rate: The percentage of deployments that cause a failure in production.
- Time to restore service: How long it takes an organization to recover from a failure in
- Time to restore service: How long it takes for an organization to recover from a failure in
production.
### Supported metrics in GitLab
@ -62,39 +55,48 @@ The following table shows the supported metrics, at which level they are support
| Metric | Level | API version | Chart (UI) version | Comments |
|---------------------------|---------------------|--------------------------------------|---------------------------------------|-----------|
| `deployment_frequency` | Project-level | [13.7+](../../api/dora/metrics.md) | [13.8+](#deployment-frequency-charts) | The [old API endpoint](../../api/dora4_project_analytics.md) was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/323713) in 13.10. |
| `deployment_frequency` | Group-level | [13.10+](../../api/dora/metrics.md) | [13.12+](#deployment-frequency-charts) | |
| `lead_time_for_changes` | Project-level | [13.10+](../../api/dora/metrics.md) | [13.11+](#lead-time-charts) | Unit in seconds. Aggregation method is median. |
| `lead_time_for_changes` | Group-level | [13.10+](../../api/dora/metrics.md) | [14.0+](#lead-time-charts) | Unit in seconds. Aggregation method is median. |
| `deployment_frequency` | Project-level | [13.7+](../../api/dora/metrics.md) | [13.8+](#view-deployment-frequency-chart) | The [old API endpoint](../../api/dora4_project_analytics.md) was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/323713) in 13.10. |
| `deployment_frequency` | Group-level | [13.10+](../../api/dora/metrics.md) | [13.12+](#view-deployment-frequency-chart) | |
| `lead_time_for_changes` | Project-level | [13.10+](../../api/dora/metrics.md) | [13.11+](#view-lead-time-for-changes-chart) | Unit in seconds. Aggregation method is median. |
| `lead_time_for_changes` | Group-level | [13.10+](../../api/dora/metrics.md) | [14.0+](#view-lead-time-for-changes-chart) | Unit in seconds. Aggregation method is median. |
| `change_failure_rate` | Project/Group-level | To be supported | To be supported | |
| `time_to_restore_service` | Project/Group-level | To be supported | To be supported | |
### Deployment frequency charts
## View deployment frequency chart **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.8.
The **Analytics > CI/CD Analytics** page shows information about the deployment
The deployment frequency charts show information about the deployment
frequency to the `production` environment. The environment must be part of the
[production deployment tier](../../ci/environments/index.md#deployment-tier-of-environments)
for its deployment information to appear on the graphs.
The deployment frequency chart is available for groups and projects.
To view the deployment frequency chart:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Analytics > CI/CD Analytics**.
1. Select the **Deployment frequency** tab.
![Deployment frequency](img/deployment_frequency_charts_v13_12.png)
These charts are available for both groups and projects.
### Lead time charts
## View lead time for changes chart **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250329) in GitLab 13.11.
The charts in the **Lead Time** tab show information about how long it takes
merge requests to be deployed to a production environment.
The lead time for changes chart shows information about how long it takes for
merge requests to be deployed to a production environment. This chart is available for groups and projects.
- Small lead times indicate fast, efficient deployment
processes.
- For time periods in which no merge requests were deployed, the charts render a
red, dashed line.
To view the lead time for changes chart:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Analytics > CI/CD Analytics**.
1. Select the **Lead time** tab.
![Lead time](img/lead_time_chart_v13_11.png)
Smaller values are better. Small lead times indicate fast, efficient deployment
processes.
For time periods in which no merge requests were deployed, the charts render a
red, dashed line.
These charts are available for both groups and projects.

View File

@ -18,7 +18,7 @@ module API
# rubocop: disable CodeReuse/ActiveRecord
def total_time_spent
# Avoids an N+1 query since timelogs are preloaded
object.timelogs.map(&:time_spent).sum
object.timelogs.sum(&:time_spent)
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -104,7 +104,7 @@ module ContainerRegistry
def total_size
return unless layers
layers.map(&:size).sum if v2?
layers.sum(&:size) if v2?
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -106,7 +106,7 @@ module Gitlab
private
def max_tests(*used)
[DEFAULT_MAX_TESTS - used.map(&:count).sum, DEFAULT_MIN_TESTS].max
[DEFAULT_MAX_TESTS - used.sum(&:count), DEFAULT_MIN_TESTS].max
end
end
end

View File

@ -296,13 +296,13 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def size
valid_blobs.map(&:size).sum
valid_blobs.sum(&:size)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def raw_size
valid_blobs.map(&:raw_size).sum
valid_blobs.sum(&:raw_size)
end
# rubocop: enable CodeReuse/ActiveRecord

View File

@ -0,0 +1,5 @@
- name: ci_users_executing_deployment_job
category: ci_users
redis_slot: ci_users
aggregation: weekly
feature_flag: job_deployment_count

View File

@ -23,7 +23,7 @@ module Peek
private
def duration
detail_store.map { |entry| entry[:duration] }.sum * 1000
detail_store.sum { |entry| entry[:duration] } * 1000
end
def calls

View File

@ -192,6 +192,7 @@ namespace :gitlab do
end
Rake::Task['db:test:purge'].enhance(['gitlab:db:clear_all_connections'])
Rake::Task['db:drop'].enhance(['gitlab:db:clear_all_connections'])
# During testing, db:test:load restores the database schema from scratch
# which does not include dynamic partitions. We cannot rely on application

View File

@ -47,13 +47,15 @@ namespace :gitlab do
# will work.
def self.terminate_all_connections
cmd = <<~SQL
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE datname = current_database()
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE datname = current_database()
AND pid <> pg_backend_pid();
SQL
ActiveRecord::Base.connection.execute(cmd)&.result_status == PG::PGRES_TUPLES_OK
rescue ActiveRecord::NoDatabaseError
Gitlab::Database::EachDatabase.each_database_connection do |connection|
connection.execute(cmd)
rescue ActiveRecord::NoDatabaseError
end
end
end

View File

@ -13214,6 +13214,9 @@ msgstr ""
msgid "Edit in Web IDE"
msgstr ""
msgid "Edit in pipeline editor"
msgstr ""
msgid "Edit in single-file editor"
msgstr ""
@ -13256,6 +13259,9 @@ msgstr ""
msgid "Edit your most recent comment in a thread (from an empty textarea)"
msgstr ""
msgid "Edit, lint, and visualize your pipeline."
msgstr ""
msgid "Edited"
msgstr ""

View File

@ -1211,16 +1211,6 @@ RSpec.describe ProjectsController do
expect(response).to have_gitlab_http_status(:success)
end
context 'when "strong_parameters_for_project_controller" FF is disabled' do
before do
stub_feature_flags(strong_parameters_for_project_controller: false)
end
it 'raises an exception' do
expect { request }.to raise_error(TypeError)
end
end
end
end

View File

@ -12,6 +12,7 @@ import {
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
} from '~/security_configuration/constants';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/utils/optimistic_response';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
@ -159,17 +160,6 @@ describe('TrainingProviderList component', () => {
await toggleFirstProvider();
});
it.each`
loading | wait | desc
${true} | ${false} | ${'enables loading of GlToggle when mutation is called'}
${false} | ${true} | ${'disables loading of GlToggle when mutation is complete'}
`('$desc', async ({ loading, wait }) => {
if (wait) {
await waitForMutationToBeLoaded();
}
expect(findFirstToggle().props('isLoading')).toBe(loading);
});
it('calls mutation when toggle is changed', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
@ -186,6 +176,20 @@ describe('TrainingProviderList component', () => {
);
});
it('returns an optimistic response when calling the mutation', () => {
const optimisticResponse = updateSecurityTrainingOptimisticResponse({
id: securityTrainingProviders[0].id,
isEnabled: true,
isPrimary: false,
});
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
optimisticResponse,
}),
);
});
it('dismisses the callout when the feature gets first enabled', async () => {
// wait for configuration update mutation to complete
await waitForMutationToBeLoaded();

View File

@ -12,6 +12,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
const TEST_PIPELINE_EDITOR_URL = '/-/ci/editor?branch_name="main"';
const TEST_USER_PREFERENCES_GITPOD_PATH = '/-/profile/preferences#user_gitpod_enabled';
const TEST_USER_PROFILE_ENABLE_GITPOD_PATH = '/-/profile?user%5Bgitpod_enabled%5D=true';
const forkPath = '/some/fork/path';
@ -66,6 +67,16 @@ const ACTION_GITPOD_ENABLE = {
href: undefined,
handle: expect.any(Function),
};
const ACTION_PIPELINE_EDITOR = {
href: TEST_PIPELINE_EDITOR_URL,
key: 'pipeline_editor',
secondaryText: 'Edit, lint, and visualize your pipeline.',
tooltip: 'Edit, lint, and visualize your pipeline.',
text: 'Edit in pipeline editor',
attrs: {
'data-qa-selector': 'pipeline_editor_button',
},
};
describe('Web IDE link component', () => {
let wrapper;
@ -76,6 +87,7 @@ describe('Web IDE link component', () => {
editUrl: TEST_EDIT_URL,
webIdeUrl: TEST_WEB_IDE_URL,
gitpodUrl: TEST_GITPOD_URL,
pipelineEditorUrl: TEST_PIPELINE_EDITOR_URL,
forkPath,
...props,
},
@ -106,6 +118,10 @@ describe('Web IDE link component', () => {
props: {},
expectedActions: [ACTION_WEB_IDE, ACTION_EDIT],
},
{
props: { showPipelineEditorButton: true },
expectedActions: [ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_EDIT],
},
{
props: { webIdeText: 'Test Web IDE' },
expectedActions: [{ ...ACTION_WEB_IDE_EDIT_FORK, text: 'Test Web IDE' }, ACTION_EDIT],
@ -193,12 +209,34 @@ describe('Web IDE link component', () => {
expect(findActionsButton().props('actions')).toEqual(expectedActions);
});
describe('when pipeline editor action is available', () => {
beforeEach(() => {
createComponent({
showEditButton: false,
showWebIdeButton: true,
showGitpodButton: true,
showPipelineEditorButton: true,
userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
gitpodEnabled: true,
});
});
it('selected Pipeline Editor by default', () => {
expect(findActionsButton().props()).toMatchObject({
actions: [ACTION_PIPELINE_EDITOR, ACTION_WEB_IDE, ACTION_GITPOD],
selectedKey: ACTION_PIPELINE_EDITOR.key,
});
});
});
describe('with multiple actions', () => {
beforeEach(() => {
createComponent({
showEditButton: false,
showWebIdeButton: true,
showGitpodButton: true,
showPipelineEditorButton: false,
userPreferencesGitpodPath: TEST_USER_PREFERENCES_GITPOD_PATH,
userProfileEnableGitpodPath: TEST_USER_PROFILE_ENABLE_GITPOD_PATH,
gitpodEnabled: true,
@ -240,6 +278,7 @@ describe('Web IDE link component', () => {
props: {
showWebIdeButton: true,
showEditButton: false,
showPipelineEditorButton: false,
forkPath,
forkModalId: 'edit-modal',
},
@ -249,6 +288,7 @@ describe('Web IDE link component', () => {
props: {
showWebIdeButton: false,
showEditButton: true,
showPipelineEditorButton: false,
forkPath,
forkModalId: 'webide-modal',
},

View File

@ -116,9 +116,11 @@ RSpec.describe TreeHelper do
show_edit_button: false,
show_web_ide_button: true,
show_gitpod_button: false,
show_pipeline_editor_button: false,
edit_url: '',
web_ide_url: "/-/ide/project/#{project.full_path}/edit/#{sha}",
pipeline_editor_url: "/#{project.full_path}/-/ci/editor?branch_name=#{@ref}",
gitpod_url: '',
user_preferences_gitpod_path: user_preferences_gitpod_path,

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WebIdeButtonHelper do
describe '#show_pipeline_editor_button?' do
subject(:result) { helper.show_pipeline_editor_button?(project, path) }
let_it_be(:project) { build(:project) }
context 'when can view pipeline editor' do
before do
allow(helper).to receive(:can_view_pipeline_editor?).and_return(true)
end
context 'when path is ci config path' do
let(:path) { project.ci_config_path_or_default }
it 'returns true' do
expect(result).to eq(true)
end
end
context 'when path is not config path' do
let(:path) { '/' }
it 'returns false' do
expect(result).to eq(false)
end
end
end
context 'when can not view pipeline editor' do
before do
allow(helper).to receive(:can_view_pipeline_editor?).and_return(false)
end
let(:path) { project.ci_config_path_or_default }
it 'returns false' do
expect(result).to eq(false)
end
end
end
end

View File

@ -51,7 +51,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
'network_policies',
'geo',
'growth',
'work_items'
'work_items',
'ci_users'
)
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::HasDeploymentName do
describe 'deployment_name?' do
let(:build) { create(:ci_build) }
subject { build.branch? }
it 'does detect deployment names' do
build.name = 'deployment'
expect(build.deployment_name?).to be_truthy
end
it 'does detect partial deployment names' do
build.name = 'do a really cool deploy'
expect(build.deployment_name?).to be_truthy
end
it 'does not detect non-deployment names' do
build.name = 'testing'
expect(build.deployment_name?).to be_falsy
end
it 'is case insensitive' do
build.name = 'DEPLOY'
expect(build.deployment_name?).to be_truthy
end
end
end

View File

@ -178,7 +178,7 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
snippets = create_list(:personal_snippet, 3, :repository, author: user)
snippets.each { |s| s.statistics.refresh! }
total_personal_snippets_size = snippets.map { |s| s.statistics.repository_size }.sum
total_personal_snippets_size = snippets.sum { |s| s.statistics.repository_size }
root_storage_statistics.recalculate!

View File

@ -1,13 +1,19 @@
# frozen_string_literal: true
RSpec.shared_examples 'avoid N+1 on environments serialization' do
RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false|
# Investigating in https://gitlab.com/gitlab-org/gitlab/-/issues/353209
let(:query_threshold) { 50 + (ee ? 4 : 0) }
it 'avoids N+1 database queries with grouping', :request_store do
create_environment_with_associations(project)
control = ActiveRecord::QueryRecorder.new { serialize(grouping: true) }
create_environment_with_associations(project)
create_environment_with_associations(project)
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count)
expect { serialize(grouping: true) }
.not_to exceed_query_limit(control.count)
.with_threshold(query_threshold)
end
it 'avoids N+1 database queries without grouping', :request_store do
@ -15,9 +21,12 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
control = ActiveRecord::QueryRecorder.new { serialize(grouping: false) }
create_environment_with_associations(project)
create_environment_with_associations(project)
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count)
expect { serialize(grouping: false) }
.not_to exceed_query_limit(control.count)
.with_threshold(query_threshold)
end
it 'does not preload for environments that does not exist in the page', :request_store do
@ -35,7 +44,7 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
end
def serialize(grouping:, query: nil)
query ||= { page: 1, per_page: 1 }
query ||= { page: 1, per_page: 20 }
request = double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query)
EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer|

View File

@ -0,0 +1,141 @@
# frozen_string_literal: true
require 'rake_helper'
RSpec.describe 'gitlab:setup namespace rake tasks', :silence_stdout do
before do
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
Rake.application.rake_require 'tasks/gitlab/setup'
end
describe 'setup' do
subject(:setup_task) { run_rake_task('gitlab:setup') }
let(:storages) do
{
'name1' => 'some details',
'name2' => 'other details'
}
end
let(:server_service1) { double(:server_service) }
let(:server_service2) { double(:server_service) }
let(:connections) { Gitlab::Database.database_base_models.values.map(&:connection) }
before do
allow(Gitlab).to receive_message_chain('config.repositories.storages').and_return(storages)
stub_warn_user_is_not_gitlab
allow(main_object).to receive(:ask_to_continue)
end
it 'sets up the application', :aggregate_failures do
expect_gitaly_connections_to_be_checked
expect_connections_to_be_terminated
expect_database_to_be_setup
setup_task
end
context 'when an environment variable is set to force execution' do
before do
stub_env('force', 'yes')
end
it 'sets up the application without prompting the user', :aggregate_failures do
expect_gitaly_connections_to_be_checked
expect(main_object).not_to receive(:ask_to_continue)
expect_connections_to_be_terminated
expect_database_to_be_setup
setup_task
end
end
context 'when the gitaly connection check raises an error' do
it 'exits the task without setting up the database', :aggregate_failures do
expect(Gitlab::GitalyClient::ServerService).to receive(:new).with('name1').and_return(server_service1)
expect(server_service1).to receive(:info).and_raise(GRPC::Unavailable)
expect_connections_not_to_be_terminated
expect_database_not_to_be_setup
expect { setup_task }.to output(/Failed to connect to Gitaly/).to_stdout
.and raise_error(SystemExit) { |error| expect(error.status).to eq(1) }
end
end
context 'when the task is aborted' do
it 'exits without setting up the database', :aggregate_failures do
expect_gitaly_connections_to_be_checked
expect(main_object).to receive(:ask_to_continue).and_raise(Gitlab::TaskAbortedByUserError)
expect_connections_not_to_be_terminated
expect_database_not_to_be_setup
expect { setup_task }.to output(/Quitting/).to_stdout
.and raise_error(SystemExit) { |error| expect(error.status).to eq(1) }
end
end
context 'when in the production environment' do
it 'sets up the database without terminating connections', :aggregate_failures do
expect_gitaly_connections_to_be_checked
expect(Rails.env).to receive(:production?).and_return(true)
expect_connections_not_to_be_terminated
expect_database_to_be_setup
setup_task
end
end
context 'when the database is not found when terminating connections' do
it 'continues setting up the database', :aggregate_failures do
expect_gitaly_connections_to_be_checked
expect(connections).to all(receive(:execute).and_raise(ActiveRecord::NoDatabaseError))
expect_database_to_be_setup
setup_task
end
end
def expect_gitaly_connections_to_be_checked
expect(Gitlab::GitalyClient::ServerService).to receive(:new).with('name1').and_return(server_service1)
expect(server_service1).to receive(:info)
expect(Gitlab::GitalyClient::ServerService).to receive(:new).with('name2').and_return(server_service2)
expect(server_service2).to receive(:info)
end
def expect_connections_to_be_terminated
expect(connections).to all(receive(:execute).with(/SELECT pg_terminate_backend/))
end
def expect_connections_not_to_be_terminated
connections.each do |connection|
expect(connection).not_to receive(:execute)
end
end
def expect_database_to_be_setup
expect(Rake::Task['db:reset']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
end
def expect_database_not_to_be_setup
expect(Rake::Task['db:reset']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
end
end
end

View File

@ -16,33 +16,6 @@ type PreAuthorizer interface {
PreAuthorizeHandler(next api.HandleFunc, suffix string) http.Handler
}
// Verifier is an optional pluggable behavior for upload paths. If
// Verify() returns an error, Workhorse will return an error response to
// the client instead of propagating the request to Rails. The motivating
// use case is Git LFS, where Workhorse checks the size and SHA256
// checksum of the uploaded file.
type Verifier interface {
// Verify can abort the upload by returning an error
Verify(handler *filestore.FileHandler) error
}
// Preparer is a pluggable behavior that interprets a Rails API response
// and either tells Workhorse how to handle the upload, via the
// SaveFileOpts and Verifier, or it rejects the request by returning a
// non-nil error. Its intended use is to make sure the upload gets stored
// in the right location: either a local directory, or one of several
// supported object storage backends.
type Preparer interface {
Prepare(a *api.Response) (*filestore.SaveFileOpts, Verifier, error)
}
type DefaultPreparer struct{}
func (s *DefaultPreparer) Prepare(a *api.Response) (*filestore.SaveFileOpts, Verifier, error) {
opts, err := filestore.GetOpts(a)
return opts, nil, err
}
// RequestBody is a request middleware. It will store the request body to
// a location by determined an api.Response value. It then forwards the
// request to gitlab-rails without the original request body.

View File

@ -1,8 +1,4 @@
/*
In this file we handle git lfs objects downloads and uploads
*/
package lfs
package upload
import (
"fmt"
@ -10,7 +6,6 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/filestore"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload"
)
type object struct {
@ -31,14 +26,16 @@ func (l *object) Verify(fh *filestore.FileHandler) error {
}
type uploadPreparer struct {
objectPreparer upload.Preparer
objectPreparer Preparer
}
func NewLfsUploadPreparer(c config.Config, objectPreparer upload.Preparer) upload.Preparer {
// NewLfs returns a new preparer instance which adds capability to a wrapped
// preparer to set options required for a LFS upload.
func NewLfsPreparer(c config.Config, objectPreparer Preparer) Preparer {
return &uploadPreparer{objectPreparer: objectPreparer}
}
func (l *uploadPreparer) Prepare(a *api.Response) (*filestore.SaveFileOpts, upload.Verifier, error) {
func (l *uploadPreparer) Prepare(a *api.Response) (*filestore.SaveFileOpts, Verifier, error) {
opts, _, err := l.objectPreparer.Prepare(a)
if err != nil {
return nil, nil, err

View File

@ -1,17 +1,15 @@
package lfs_test
package upload
import (
"testing"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/lfs"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload"
"github.com/stretchr/testify/require"
)
func TestLfsUploadPreparerWithConfig(t *testing.T) {
func TestLfsPreparerWithConfig(t *testing.T) {
lfsOid := "abcd1234"
creds := config.S3Credentials{
AwsAccessKeyID: "test-key",
@ -36,8 +34,8 @@ func TestLfsUploadPreparerWithConfig(t *testing.T) {
},
}
uploadPreparer := upload.NewObjectStoragePreparer(c)
lfsPreparer := lfs.NewLfsUploadPreparer(c, uploadPreparer)
uploadPreparer := NewObjectStoragePreparer(c)
lfsPreparer := NewLfsPreparer(c, uploadPreparer)
opts, verifier, err := lfsPreparer.Prepare(r)
require.NoError(t, err)
@ -48,11 +46,11 @@ func TestLfsUploadPreparerWithConfig(t *testing.T) {
require.NotNil(t, verifier)
}
func TestLfsUploadPreparerWithNoConfig(t *testing.T) {
func TestLfsPreparerWithNoConfig(t *testing.T) {
c := config.Config{}
r := &api.Response{RemoteObject: api.RemoteObject{ID: "the upload ID"}}
uploadPreparer := upload.NewObjectStoragePreparer(c)
lfsPreparer := lfs.NewLfsUploadPreparer(c, uploadPreparer)
uploadPreparer := NewObjectStoragePreparer(c)
lfsPreparer := NewLfsPreparer(c, uploadPreparer)
opts, verifier, err := lfsPreparer.Prepare(r)
require.NoError(t, err)

View File

@ -11,6 +11,9 @@ type ObjectStoragePreparer struct {
credentials config.ObjectStorageCredentials
}
// NewObjectStoragePreparer returns a new preparer instance which is responsible for
// setting the object storage credentials and settings needed by an uploader
// to upload to object storage.
func NewObjectStoragePreparer(c config.Config) Preparer {
return &ObjectStoragePreparer{credentials: c.ObjectStorageCredentials, config: c.ObjectStorageConfig}
}

View File

@ -0,0 +1,33 @@
package upload
import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/filestore"
)
// Verifier is an optional pluggable behavior for upload paths. If
// Verify() returns an error, Workhorse will return an error response to
// the client instead of propagating the request to Rails. The motivating
// use case is Git LFS, where Workhorse checks the size and SHA256
// checksum of the uploaded file.
type Verifier interface {
// Verify can abort the upload by returning an error
Verify(handler *filestore.FileHandler) error
}
// Preparer is a pluggable behavior that interprets a Rails API response
// and either tells Workhorse how to handle the upload, via the
// SaveFileOpts and Verifier, or it rejects the request by returning a
// non-nil error. Its intended use is to make sure the upload gets stored
// in the right location: either a local directory, or one of several
// supported object storage backends.
type Preparer interface {
Prepare(a *api.Response) (*filestore.SaveFileOpts, Verifier, error)
}
type DefaultPreparer struct{}
func (s *DefaultPreparer) Prepare(a *api.Response) (*filestore.SaveFileOpts, Verifier, error) {
opts, err := filestore.GetOpts(a)
return opts, nil, err
}

View File

@ -20,7 +20,6 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/git"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/imageresizer"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/lfs"
proxypkg "gitlab.com/gitlab-org/gitlab/workhorse/internal/proxy"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/queueing"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/redis"
@ -408,7 +407,7 @@ func createUploadPreparers(cfg config.Config) uploadPreparers {
return uploadPreparers{
artifacts: defaultPreparer,
lfs: lfs.NewLfsUploadPreparer(cfg, defaultPreparer),
lfs: upload.NewLfsPreparer(cfg, defaultPreparer),
packages: defaultPreparer,
uploads: defaultPreparer,
}