Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
563c8efdee
commit
e40c68997d
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
- name: ci_users_executing_deployment_job
|
||||
category: ci_users
|
||||
redis_slot: ci_users
|
||||
aggregation: weekly
|
||||
feature_flag: job_deployment_count
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue