Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-14 18:16:21 +00:00
parent 1847ddf46c
commit 0d1ffbe0b6
110 changed files with 465 additions and 3193 deletions

View File

@ -18,6 +18,10 @@ stages:
- GIT_DEPTH
- GIT_STRATEGY
workflow:
auto_cancel:
on_new_commit: none
variables:
GIT_DEPTH: 20
GIT_STRATEGY: fetch
@ -62,11 +66,16 @@ release-environments-deploy:
variables:
VERSIONS: "${VERSIONS}"
ENVIRONMENT: "${ENVIRONMENT}"
before_script:
# Make sure pipelines run in order
# See https://docs.gitlab.com/ee/ci/resource_groups/index.html#change-the-process-mode
- curl --request PUT --data "process_mode=oldest_first" --header "PRIVATE-TOKEN:${ENVIRONMENT_API_TOKEN}" "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/resource_groups/release-environment-${CI_COMMIT_REF_SLUG}"
trigger:
project: gitlab-com/gl-infra/release-environments
branch: main
strategy: depend
needs: ["release-environments-deploy-env"]
resource_group: release-environment-${CI_COMMIT_REF_SLUG}
release-environments-qa:
stage: qa
@ -80,6 +89,7 @@ release-environments-qa:
GITLAB_INITIAL_ROOT_PASSWORD: "${RELEASE_ENVIRONMENTS_ROOT_PASSWORD}"
QA_PRAEFECT_REPOSITORY_STORAGE: "default"
SIGNUP_DISABLED: "true"
resource_group: release-environment-${CI_COMMIT_REF_SLUG}
release-environments-notification-failure:
stage: finish

View File

@ -341,7 +341,6 @@
- "{,ee/,jh/}{,spec/}app/models/ci/build_trace_chunks/redis{,_base,_trace_chunks}{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/usage_data_counters/{hll_redis_counter,redis_counter}{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/usage/metrics/instrumentations/redis{_metric,hll_metric}{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/usage/metrics/aggregates/sources/redis_hll{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/merge_requests/mergeability/redis_interface{,_spec}.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/markdown_cache/redis/*.rb"
- "{,ee/,jh/}{,spec/}lib/{,ee/,jh/}gitlab/redis/**/*.rb"

View File

@ -3,18 +3,4 @@
Layout/LineContinuationLeadingSpace:
Exclude:
- 'ee/lib/tasks/gitlab/geo.rake'
- 'lib/gitlab/background_migration/populate_operation_visibility_permissions_from_operations.rb'
- 'lib/gitlab/ci/parsers/security/validators/schema_validator.rb'
- 'lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb'
- 'lib/gitlab/database/partitioning_migration_helpers/index_helpers.rb'
- 'lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb'
- 'lib/gitlab/github_import/importer/events/changed_reviewer.rb'
- 'lib/gitlab/import_export/project/import_task.rb'
- 'lib/gitlab/reference_counter.rb'
- 'lib/gitlab/tracking/standard_context.rb'
- 'qa/qa/specs/features/api/4_verify/api_variable_inheritance_with_forward_pipeline_variables_spec.rb'
- 'rubocop/cop/graphql/descriptions.rb'
- 'rubocop/cop/migration/add_columns_to_wide_tables.rb'
- 'rubocop/cop/migration/background_migrations.rb'
- 'scripts/lib/glfm/parse_examples.rb'
- 'scripts/qa/testcases-check'

View File

@ -2645,7 +2645,6 @@ Layout/LineLength:
- 'lib/gitlab/tracking/destinations/snowplow.rb'
- 'lib/gitlab/tracking/event_definition.rb'
- 'lib/gitlab/usage/metric_definition.rb'
- 'lib/gitlab/usage/metrics/aggregates/aggregate.rb'
- 'lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb'
- 'lib/gitlab/usage/service_ping_report.rb'
- 'lib/gitlab/usage_data.rb'
@ -3767,9 +3766,7 @@ Layout/LineLength:
- 'spec/lib/gitlab/url_builder_spec.rb'
- 'spec/lib/gitlab/usage/metric_definition_spec.rb'
- 'spec/lib/gitlab/usage/metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb'
- 'spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb'
- 'spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb'

View File

@ -1,5 +0,0 @@
---
# Cop supports --autocorrect.
Layout/SpaceInsidePercentLiteralDelimiters:
Exclude:
- 'spec/deprecation_toolkit_env.rb'

View File

@ -1,6 +0,0 @@
---
# Cop supports --autocorrect.
Lint/DeprecatedConstants:
Exclude:
- 'scripts/pipeline_test_report_builder.rb'
- 'spec/scripts/pipeline_test_report_builder_spec.rb'

View File

@ -506,7 +506,6 @@ Lint/UnusedMethodArgument:
- 'lib/gitlab/testing/robots_blocker_middleware.rb'
- 'lib/gitlab/tracking.rb'
- 'lib/gitlab/untrusted_regexp/ruby_syntax.rb'
- 'lib/gitlab/usage/metrics/aggregates/sources/redis_hll.rb'
- 'lib/gitlab/usage_data.rb'
- 'lib/gitlab/usage_data_non_sql_metrics.rb'
- 'lib/gitlab/usage_data_queries.rb'

View File

@ -617,7 +617,6 @@ RSpec/FeatureCategory:
- 'ee/spec/lib/ee/gitlab/snippet_search_results_spec.rb'
- 'ee/spec/lib/ee/gitlab/template/gitlab_ci_yml_template_spec.rb'
- 'ee/spec/lib/ee/gitlab/url_builder_spec.rb'
- 'ee/spec/lib/ee/gitlab/usage/metrics/aggregates/aggregate_spec.rb'
- 'ee/spec/lib/ee/gitlab/usage/service_ping/payload_keys_processor_spec.rb'
- 'ee/spec/lib/ee/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb'
- 'ee/spec/lib/ee/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb'
@ -3921,11 +3920,8 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/url_sanitizer_spec.rb'
- 'spec/lib/gitlab/usage/metric_definition_spec.rb'
- 'spec/lib/gitlab/usage/metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb'
- 'spec/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll_spec.rb'
- 'spec/lib/gitlab/usage/metrics/aggregates/sources/redis_hll_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/active_user_count_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/aggregated_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/cert_based_clusters_ff_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/collected_data_categories_metric_spec.rb'
- 'spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb'

View File

@ -1 +1 @@
677e0ab263cf525c6e9557d33908cf55f0a13bd2
7289808c830930bbb1009d7acc5eec458dcdf210

View File

@ -203,9 +203,9 @@ gem 'seed-fu', '~> 2.3.7' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-model', '~> 7.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-api', '7.13.3' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-core', '~> 3.194.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-core', '~> 3.196.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-s3', '~> 1.149.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-s3', '~> 1.150.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'faraday_middleware-aws-sigv4', '~>0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'typhoeus', '~> 1.4.0' # Used with Elasticsearch to support http keep-alive connections # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -36,9 +36,9 @@
{"name":"aws-eventstream","version":"1.3.0","platform":"ruby","checksum":"f1434cc03ab2248756eb02cfa45e900e59a061d7fbdc4a9fd82a5dd23d796d3f"},
{"name":"aws-partitions","version":"1.877.0","platform":"ruby","checksum":"9552ed7bbd3700ed1eeb0121c160ceaf64fa5dbaff5a1ff5fe6fd8481ecd9cfd"},
{"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"},
{"name":"aws-sdk-core","version":"3.194.2","platform":"ruby","checksum":"f925fb739cd093e5834910aed85aba5ac8d1b210f26c2cf51f0daf932cc77567"},
{"name":"aws-sdk-core","version":"3.196.0","platform":"ruby","checksum":"a9a8ce8cc133eb80ba6e78c4eb3f2a04b7c74d79962ad7b24e7f5d803ee717a1"},
{"name":"aws-sdk-kms","version":"1.76.0","platform":"ruby","checksum":"e7f75013cba9ba357144f66bbc600631c192e2cda9dd572794be239654e2cf49"},
{"name":"aws-sdk-s3","version":"1.149.1","platform":"ruby","checksum":"664e608190d42b486dc79b5dc65e7c2240923902a9833063327a9d831226a46a"},
{"name":"aws-sdk-s3","version":"1.150.0","platform":"ruby","checksum":"1ce0d42f6c53de4244e457d92296da53bfbd12e716b50d2fa851255fe936c82c"},
{"name":"aws-sigv4","version":"1.8.0","platform":"ruby","checksum":"84dd99768b91b93b63d1d8e53ee837cfd06ab402812772a7899a78f9f9117cbc"},
{"name":"axe-core-api","version":"4.8.0","platform":"ruby","checksum":"88cf44fdbd5d501ae429f9ca6b37c4a46ba27ac673d478ab688eea3e353da62f"},
{"name":"axe-core-rspec","version":"4.9.0","platform":"ruby","checksum":"e5f81fa55af0c421254c98476511c4511e193c5659996f184541f74a1359df3a"},

View File

@ -303,7 +303,7 @@ GEM
aws-sdk-cloudformation (1.41.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sdk-core (3.194.2)
aws-sdk-core (3.196.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
@ -311,7 +311,7 @@ GEM
aws-sdk-kms (1.76.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.149.1)
aws-sdk-s3 (1.150.0)
aws-sdk-core (~> 3, >= 3.194.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
@ -1921,8 +1921,8 @@ DEPENDENCIES
attr_encrypted (~> 3.2.4)!
awesome_print
aws-sdk-cloudformation (~> 1)
aws-sdk-core (~> 3.194.2)
aws-sdk-s3 (~> 1.149.1)
aws-sdk-core (~> 3.196.0)
aws-sdk-s3 (~> 1.150.0)
axe-core-rspec (~> 4.9.0)
babosa (~> 2.0)
base32 (~> 0.3.0)

View File

@ -5,6 +5,7 @@ import FilteredSearchAndSort from '~/groups_projects/components/filtered_search_
import { RECENT_SEARCHES_STORAGE_KEY_PROJECTS } from '~/filtered_search/recent_searches_storage_keys';
import { queryToObject, objectToQuery, visitUrl } from '~/lib/utils/url_utility';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { ACCESS_LEVEL_OWNER_INTEGER } from '~/access_level/constants';
import {
SORT_OPTIONS,
SORT_DIRECTION_ASC,
@ -41,6 +42,21 @@ export default {
title: name,
})),
},
{
type: 'min_access_level',
icon: 'user',
title: __('Role'),
token: GlFilteredSearchToken,
unique: true,
operators: OPERATORS_IS,
options: [
{
// Cast to string so it matches value from query string
value: ACCESS_LEVEL_OWNER_INTEGER.toString(),
title: __('Owner'),
},
],
},
];
},
queryAsObject() {

View File

@ -1,58 +0,0 @@
<script>
import { s__ } from '~/locale';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import StateContainer from '../state_container.vue';
import { DETAILED_MERGE_STATUS } from '../../constants';
export default {
i18n: {
approvalNeeded: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} all required approvals must be given.',
),
blockingMergeRequests: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} you can only merge after the above items are resolved.',
),
externalStatusChecksFailed: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} all status checks must pass.',
),
},
components: {
BoldText,
StateContainer,
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
failedText() {
if (this.mr.approvals && !this.mr.isApproved) {
return this.$options.i18n.approvalNeeded;
}
if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) {
return this.$options.i18n.blockingMergeRequests;
}
if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS) {
return this.$options.i18n.externalStatusChecksFailed;
}
return null;
},
},
};
</script>
<template>
<state-container
status="failed"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<span class="gl-ml-3 gl-gl-w-full gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!">
<bold-text :message="failedText" />
</span>
</state-container>
</template>

View File

@ -24,12 +24,7 @@ export default {
</script>
<template>
<state-container
status="failed"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<state-container status="failed" is-collapsible>
<bold-text :message="$options.message" />
</state-container>
</template>

View File

@ -130,14 +130,7 @@ export default {
};
</script>
<template>
<state-container
status="scheduled"
:is-loading="loading"
:actions="actions"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<state-container status="scheduled" :is-loading="loading" :actions="actions" is-collapsible>
<template #loading>
<gl-skeleton-loader :width="334" :height="24">
<rect x="0" y="0" width="24" height="24" rx="4" />

View File

@ -54,13 +54,7 @@ export default {
};
</script>
<template>
<state-container
status="failed"
:actions="actions"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<state-container status="failed" :actions="actions" is-collapsible>
<span class="gl-font-weight-bold">
<template v-if="mergeError">{{ mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }}

View File

@ -15,12 +15,7 @@ export default {
};
</script>
<template>
<state-container
status="loading"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<state-container status="loading" is-collapsible>
{{ s__('mrWidget|Checking if merge request can be merged…') }}
</state-container>
</template>

View File

@ -79,13 +79,7 @@ export default {
};
</script>
<template>
<state-container
status="closed"
:actions="actions"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<state-container status="closed" :actions="actions" is-collapsible>
<mr-widget-author-time
:action-text="s__('mrWidget|Closed by')"
:author="mr.metrics.closedBy"

View File

@ -1,123 +0,0 @@
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { s__ } from '~/locale';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import userPermissionsQuery from '../../queries/permissions.query.graphql';
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
import StateContainer from '../state_container.vue';
export default {
name: 'MRWidgetConflicts',
components: {
BoldText,
GlSkeletonLoader,
GlButton,
StateContainer,
},
mixins: [mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
query: userPermissionsQuery,
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest?.userPermissions || {},
},
state: {
query: conflictsStateQuery,
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest || {},
},
},
props: {
/* TODO: This is providing all store and service down when it
only needs a few props */
mr: {
type: Object,
required: true,
},
},
data() {
return {
userPermissions: {},
state: {},
};
},
computed: {
isLoading() {
return this.$apollo.queries.userPermissions.loading && this.$apollo.queries.state.loading;
},
showResolveButton() {
return (
this.mr.conflictResolutionPath &&
this.userPermissions.pushToSourceBranch &&
!this.state.sourceBranchProtected
);
},
},
i18n: {
shouldBeRebased: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} fast-forward merge is not possible. To merge this request, first rebase locally.',
),
shouldBeResolved: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} merge conflicts must be resolved.',
),
usersWriteBranches: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} Users who can write to the source or target branches can resolve the conflicts.',
),
},
};
</script>
<template>
<state-container
status="failed"
:is-loading="isLoading"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<template #loading>
<gl-skeleton-loader :width="334" :height="24">
<rect x="0" y="0" width="24" height="24" rx="4" />
<rect x="32" y="2" width="150" height="20" rx="4" />
<rect x="190" y="2" width="144" height="20" rx="4" />
</gl-skeleton-loader>
</template>
<template v-if="!isLoading">
<span v-if="state.shouldBeRebased" class="gl-ml-0! gl-text-body!">
<bold-text :message="$options.i18n.shouldBeRebased" />
</span>
<template v-else>
<span class="gl-ml-0! gl-text-body! gl-flex-grow-1 gl-w-full gl-md-w-auto gl-mr-2">
<bold-text v-if="userPermissions.canMerge" :message="$options.i18n.shouldBeResolved" />
<bold-text v-else :message="$options.i18n.usersWriteBranches" />
</span>
</template>
</template>
<template v-if="!isLoading && !state.shouldBeRebased" #actions>
<gl-button
v-if="showResolveButton"
:href="mr.conflictResolutionPath"
size="small"
variant="confirm"
class="gl-align-self-start"
data-testid="resolve-conflicts-button"
>
{{ s__('mrWidget|Resolve conflicts') }}
</gl-button>
<gl-button
v-if="userPermissions.canMerge"
size="small"
variant="confirm"
category="tertiary"
data-testid="merge-locally-button"
class="js-check-out-modal-trigger gl-align-self-start"
>
{{ s__('mrWidget|Resolve locally') }}
</gl-button>
</template>
</state-container>
</template>

View File

@ -150,13 +150,7 @@ export default {
};
</script>
<template>
<state-container
:actions="actions"
status="merged"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<state-container :actions="actions" status="merged" is-collapsible>
<mr-widget-author-time
:action-text="s__('mrWidget|Merged by')"
:author="mr.metrics.mergedBy"

View File

@ -1,27 +0,0 @@
<script>
import { s__ } from '~/locale';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import StatusIcon from '../mr_widget_status_icon.vue';
const message = s__(
'mrWidget|%{boldStart}Ready to be merged automatically.%{boldEnd} Ask someone with write access to this repository to merge this request.',
);
export default {
name: 'MRWidgetNotAllowed',
message,
components: {
BoldText,
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body space-children">
<bold-text :message="$options.message" />
</div>
</div>
</template>

View File

@ -1,28 +0,0 @@
<script>
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import StatusIcon from '../mr_widget_status_icon.vue';
const message = s__(
"mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. It's waiting for a manual action to continue.",
);
export default {
name: 'MRWidgetPipelineBlocked',
message,
components: {
BoldText,
StatusIcon,
},
mixins: [glFeatureFlagMixin()],
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="failed" />
<div class="media-body space-children">
<bold-text :message="$options.message" />
</div>
</div>
</template>

View File

@ -1,300 +0,0 @@
<script>
import { GlButton, GlLink, GlModal, GlSkeletonLoader } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import simplePoll from '~/lib/utils/simple_poll';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import rebaseQuery from '../../queries/states/rebase.query.graphql';
import StateContainer from '../state_container.vue';
const i18n = {
rebaseError: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} the source branch must be rebased onto the target branch.',
),
};
export default {
name: 'MRWidgetRebase',
i18n,
modal: {
id: 'rebase-security-risk-modal',
title: s__('mrWidget|Are you sure you want to rebase?'),
actionPrimary: {
text: s__('mrWidget|Rebase'),
attributes: {
variant: 'danger',
},
},
actionCancel: {
text: __('Cancel'),
attributes: {
variant: 'default',
},
},
},
runPipelinesInTheParentProjectHelpPath: helpPagePath(
'/ci/pipelines/merge_request_pipelines.html',
{
anchor: 'run-pipelines-in-the-parent-project',
},
),
apollo: {
state: {
query: rebaseQuery,
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest || {},
},
},
components: {
BoldText,
GlButton,
GlLink,
GlModal,
GlSkeletonLoader,
StateContainer,
},
mixins: [mergeRequestQueryVariablesMixin],
inject: {
canCreatePipelineInTargetProject: {
default: false,
},
},
props: {
mr: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
},
data() {
return {
state: {},
isMakingRequest: false,
rebasingError: null,
};
},
computed: {
isLoading() {
return this.$apollo.queries.state.loading;
},
rebaseInProgress() {
return this.state.rebaseInProgress;
},
canPushToSourceBranch() {
return this.state.userPermissions?.pushToSourceBranch || false;
},
targetBranch() {
return this.state.targetBranch;
},
status() {
if (this.isLoading) {
return undefined;
}
if (this.rebaseInProgress || this.isMakingRequest) {
return 'loading';
}
if (!this.canPushToSourceBranch && !this.rebaseInProgress) {
return 'failed';
}
return 'success';
},
showRebaseWithoutPipeline() {
return (
!this.mr.onlyAllowMergeIfPipelineSucceeds ||
(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline)
);
},
isForkMergeRequest() {
return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
},
isLatestPipelineCreatedInTargetProject() {
const latestPipeline = this.state.pipelines.nodes[0];
return latestPipeline?.project?.fullPath === this.mr.targetProjectFullPath;
},
shouldShowSecurityWarning() {
return (
this.canCreatePipelineInTargetProject &&
this.isForkMergeRequest &&
!this.isLatestPipelineCreatedInTargetProject
);
},
},
methods: {
rebase({ skipCi = false } = {}) {
this.isMakingRequest = true;
this.rebasingError = null;
this.service
.rebase({ skipCi })
.then(() => {
simplePoll(this.checkRebaseStatus);
})
.catch((error) => {
this.isMakingRequest = false;
if (error.response && error.response.data && error.response.data.merge_error) {
this.rebasingError = error.response.data.merge_error;
} else {
createAlert({
message: __('Something went wrong. Please try again.'),
});
}
});
},
rebaseWithoutCi() {
return this.rebase({ skipCi: true });
},
tryRebase() {
if (this.shouldShowSecurityWarning) {
this.$refs.modal.show();
} else {
this.rebase();
}
},
checkRebaseStatus(continuePolling, stopPolling) {
this.service
.poll()
.then((res) => res.data)
.then((res) => {
if (res.rebase_in_progress || res.should_be_rebased) {
continuePolling();
} else {
this.isMakingRequest = false;
if (res.merge_error && res.merge_error.length) {
this.rebasingError = res.merge_error;
} else {
toast(__('Rebase completed'));
}
eventHub.$emit('MRWidgetRebaseSuccess');
stopPolling();
}
})
.catch(() => {
this.isMakingRequest = false;
createAlert({
message: __('Something went wrong. Please try again.'),
});
stopPolling();
});
},
},
};
</script>
<template>
<div>
<state-container
:status="status"
:is-loading="isLoading"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<template #loading>
<gl-skeleton-loader :width="334" :height="24">
<rect x="0" y="0" width="24" height="24" rx="4" />
<rect x="32" y="2" width="302" height="20" rx="4" />
</gl-skeleton-loader>
</template>
<template v-if="!isLoading">
<span
v-if="rebaseInProgress || isMakingRequest"
class="gl-ml-0! gl-text-body!"
data-testid="rebase-message"
>{{ s__('mrWidget|Rebase in progress') }}</span
>
<span
v-if="!rebaseInProgress && !canPushToSourceBranch"
class="gl-text-body! gl-ml-0!"
data-testid="rebase-message"
>
<bold-text :message="$options.i18n.rebaseError" />
</span>
<div
v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest"
class="accept-merge-holder clearfix js-toggle-container media gl-md-display-flex gl-flex-wrap gl-flex-grow-1"
>
<span
v-if="!rebasingError"
class="gl-w-full gl-md-w-auto gl-flex-grow-1 gl-ml-0! gl-text-body! gl-md-mr-3"
data-testid="rebase-message"
>
<bold-text :message="$options.i18n.rebaseError" />
</span>
<span
v-else
class="gl-font-weight-bold danger gl-w-full gl-md-w-auto gl-flex-grow-1 gl-md-mr-3"
data-testid="rebase-message"
>{{ rebasingError }}</span
>
</div>
</template>
<template v-if="!isLoading" #actions>
<gl-button
:loading="isMakingRequest"
variant="confirm"
size="small"
data-testid="standard-rebase-button"
class="gl-align-self-start"
@click="tryRebase"
>
{{ s__('mrWidget|Rebase') }}
</gl-button>
<gl-button
v-if="showRebaseWithoutPipeline"
:loading="isMakingRequest"
variant="confirm"
size="small"
category="secondary"
data-testid="rebase-without-ci-button"
class="gl-align-self-start gl-mr-2"
@click="rebaseWithoutCi"
>
{{ s__('mrWidget|Rebase without pipeline') }}
</gl-button>
</template>
</state-container>
<gl-modal
ref="modal"
:modal-id="$options.modal.id"
:title="$options.modal.title"
:action-primary="$options.modal.actionPrimary"
:action-cancel="$options.modal.actionCancel"
@primary="rebase"
>
<p>
{{
s__(
'Pipelines|Rebasing creates a pipeline that runs code originating from a forked project merge request. Consequently there are potential security implications, such as the exposure of CI variables.',
)
}}
</p>
<p>
{{
s__(
"Pipelines|You should review the code thoroughly before running this pipeline with the parent project's CI/CD resources.",
)
}}
</p>
<p>
{{ s__('Pipelines|If you are unsure, ask a project maintainer to review it for you.') }}
</p>
<gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank">
{{ s__('Pipelines|More Information') }}
</gl-link>
</gl-modal>
</div>
</template>

View File

@ -1,57 +0,0 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'PipelineFailed',
components: {
BoldText,
GlLink,
GlSprintf,
StatusIcon,
},
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
troubleshootingDocsPath() {
return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' });
},
},
i18n: {
failedMessage: s__(
`mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. Push a commit that fixes the failure or %{linkStart}learn about other solutions.%{linkEnd}`,
),
blockedMessage: s__(
"mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. It's waiting for a manual action to continue.",
),
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="failed" />
<div class="media-body space-children">
<span>
<bold-text v-if="mr.isPipelineBlocked" :message="$options.i18n.blockedMessage" />
<gl-sprintf v-else :message="$options.i18n.failedMessage">
<template #link="{ content }">
<gl-link :href="troubleshootingDocsPath" target="_blank">
{{ content }}
</gl-link>
</template>
<template #bold="{ content }">
<span class="gl-font-weight-bold">{{ content }}</span>
</template>
</gl-sprintf>
</span>
</div>
</div>
</template>

View File

@ -317,7 +317,11 @@ export default {
return this.preferredAutoMergeStrategy === MT_MERGE_STRATEGY && this.isPipelineFailed;
},
shouldShowMergeControls() {
return this.state.userPermissions?.canMerge && this.mr.state === 'readyToMerge';
return (
this.state.userPermissions?.canMerge &&
!this.mr.autoMergeEnabled &&
this.mr.state === 'readyToMerge'
);
},
sourceBranchDeletedText() {
const isPreMerge = this.mr.state !== STATUS_MERGED;
@ -354,6 +358,9 @@ export default {
'mr.state': function mrStateWatcher() {
this.isMakingRequest = false;
},
'state.autoMergeEnabled': function mrAutoMergeEnabledWatcher() {
this.isMakingRequest = false;
},
},
mounted() {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);

View File

@ -24,12 +24,7 @@ export default {
</script>
<template>
<state-container
status="failed"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<state-container status="failed" is-collapsible>
<span
class="gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!"
data-testid="head-mismatch-content"

View File

@ -1,55 +0,0 @@
<script>
import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import notesEventHub from '~/notes/event_hub';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import StateContainer from '../state_container.vue';
const message = s__('mrWidget|%{boldStart}Merge blocked:%{boldEnd} all threads must be resolved.');
export default {
name: 'UnresolvedDiscussions',
message,
components: {
BoldText,
GlButton,
StateContainer,
},
props: {
mr: {
type: Object,
required: true,
},
},
methods: {
jumpToFirstUnresolvedDiscussion() {
notesEventHub.$emit('jumpToFirstUnresolvedDiscussion');
},
},
};
</script>
<template>
<state-container
status="failed"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<span class="gl-ml-3 gl-w-full gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!">
<bold-text :message="$options.message" />
</span>
<template #actions>
<gl-button
data-testid="jump-to-first"
class="gl-align-self-start gl-align-top"
size="small"
variant="confirm"
category="primary"
@click="jumpToFirstUnresolvedDiscussion"
>
{{ s__('mrWidget|Go to first unresolved thread') }}
</gl-button>
</template>
</state-container>
</template>

View File

@ -1,166 +0,0 @@
<script>
import { GlButton } from '@gitlab/ui';
import { produce } from 'immer';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import MergeRequest from '~/merge_request';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import getStateQuery from '../../queries/get_state.query.graphql';
import draftQuery from '../../queries/states/draft.query.graphql';
import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
import StateContainer from '../state_container.vue';
// Export for testing
export const MSG_SOMETHING_WENT_WRONG = __('Something went wrong. Please try again.');
export const MSG_MARK_READY = s__('mrWidget|Mark as ready');
export default {
name: 'WorkInProgress',
components: {
BoldText,
GlButton,
StateContainer,
},
mixins: [mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
query: draftQuery,
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest?.userPermissions || {},
},
},
props: {
mr: { type: Object, required: true },
},
data() {
return {
userPermissions: {},
isMakingRequest: false,
};
},
methods: {
handleRemoveDraft() {
const { mergeRequestQueryVariables } = this;
this.isMakingRequest = true;
this.$apollo
.mutate({
mutation: removeDraftMutation,
variables: {
...mergeRequestQueryVariables,
draft: false,
},
update(
store,
{
data: {
mergeRequestSetDraft: {
errors,
mergeRequest: { mergeableDiscussionsState, draft, title },
},
},
},
) {
if (errors?.length) {
createAlert({
message: MSG_SOMETHING_WENT_WRONG,
});
return;
}
const sourceData = store.readQuery({
query: getStateQuery,
variables: mergeRequestQueryVariables,
});
const data = produce(sourceData, (draftState) => {
draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
draftState.project.mergeRequest.draft = draft;
draftState.project.mergeRequest.title = title;
});
store.writeQuery({
query: getStateQuery,
data,
variables: mergeRequestQueryVariables,
});
},
optimisticResponse: {
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
mergeRequestSetDraft: {
__typename: 'MergeRequestSetWipPayload',
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
id: this.mr.issuableId,
mergeableDiscussionsState: true,
title: this.mr.title,
draft: false,
},
},
},
})
.then(
({
data: {
mergeRequestSetDraft: {
mergeRequest: { title },
},
},
}) => {
MergeRequest.toggleDraftStatus(title, true);
},
)
.catch(() =>
createAlert({
message: MSG_SOMETHING_WENT_WRONG,
}),
)
.finally(() => {
this.isMakingRequest = false;
});
},
},
i18n: {
removeDraftStatus: s__(
'mrWidget|%{boldStart}Merge blocked:%{boldEnd} Select %{boldStart}Mark as ready%{boldEnd} to remove it from Draft status.',
),
},
MSG_MARK_READY,
};
</script>
<template>
<state-container
status="failed"
is-collapsible
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
<span
class="gl-display-inline-flex gl-align-self-start gl-pt-2 gl-ml-0! gl-text-body! gl-flex-grow-1"
>
<bold-text :message="$options.i18n.removeDraftStatus" />
</span>
<template #actions>
<gl-button
v-if="userPermissions.updateMergeRequest"
size="small"
:disabled="isMakingRequest"
:loading="isMakingRequest"
variant="confirm"
class="js-remove-draft gl-md-ml-3 gl-align-self-start"
data-testid="removeWipButton"
@click="handleRemoveDraft"
>
{{ $options.MSG_MARK_READY }}
</gl-button>
</template>
</state-container>
</template>

View File

@ -24,20 +24,13 @@ import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue
import CheckingState from './components/states/mr_widget_checking.vue';
import PreparingState from './components/states/mr_widget_preparing.vue';
import ClosedState from './components/states/mr_widget_closed.vue';
import ConflictsState from './components/states/mr_widget_conflicts.vue';
import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
import MergedState from './components/states/mr_widget_merged.vue';
import MergingState from './components/states/mr_widget_merging.vue';
import MissingBranchState from './components/states/mr_widget_missing_branch.vue';
import NotAllowedState from './components/states/mr_widget_not_allowed.vue';
import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue';
import RebaseState from './components/states/mr_widget_rebase.vue';
import NothingToMergeState from './components/states/nothing_to_merge.vue';
import PipelineFailedState from './components/states/pipeline_failed.vue';
import ReadyToMergeState from './components/states/ready_to_merge.vue';
import ShaMismatch from './components/states/sha_mismatch.vue';
import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
import WorkInProgressState from './components/states/work_in_progress.vue';
import WidgetContainer from './components/widget/app.vue';
import {
STATE_MACHINE,
@ -71,25 +64,17 @@ export default {
MrWidgetClosed: ClosedState,
MrWidgetMerging: MergingState,
MrWidgetFailedToMerge: FailedToMerge,
MrWidgetWip: WorkInProgressState,
MrWidgetArchived: ArchivedState,
MrWidgetConflicts: ConflictsState,
MrWidgetNothingToMerge: NothingToMergeState,
MrWidgetNotAllowed: NotAllowedState,
MrWidgetMissingBranch: MissingBranchState,
MrWidgetReadyToMerge,
ShaMismatch,
MrWidgetChecking: CheckingState,
MrWidgetPreparing: PreparingState,
MrWidgetUnresolvedDiscussions: UnresolvedDiscussionsState,
MrWidgetPipelineBlocked: PipelineBlockedState,
MrWidgetPipelineFailed: PipelineFailedState,
MrWidgetAutoMergeEnabled,
MrWidgetAutoMergeFailed: AutoMergeFailed,
MrWidgetRebase: RebaseState,
SourceBranchRemovalStatus,
MrWidgetApprovals,
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
ReadyToMerge: ReadyToMergeState,
ReportWidgetContainer,
MergeChecks,
@ -243,29 +228,24 @@ export default {
hasAlerts() {
return this.hasMergeError || this.showMergePipelineForkWarning;
},
shouldShowMergeDetails() {
if (this.mr.state === 'readyToMerge') return true;
return !this.mr.mergeDetailsCollapsed;
},
mergeBlockedComponentEnabled() {
return (
window.gon?.features?.mergeBlockedComponent &&
!(
[
'checking',
'preparing',
'nothingToMerge',
'archived',
'missingBranch',
'merged',
'closed',
'merging',
'shaMismatch',
].includes(this.mr.state) || this.mr.machineValue === 'MERGING'
)
mergeBlockedComponentVisible() {
return !(
[
'checking',
'preparing',
'nothingToMerge',
'archived',
'missingBranch',
'merged',
'closed',
'merging',
'shaMismatch',
].includes(this.mr.state) || this.mr.machineValue === 'MERGING'
);
},
autoMergeEnabled() {
return this.mr.autoMergeEnabled;
},
},
watch: {
'mr.machineValue': {
@ -338,12 +318,6 @@ export default {
this.bindEventHubListeners();
eventHub.$on('mr.discussion.updated', this.checkStatus);
window.addEventListener('resize', () => {
if (window.innerWidth >= 768) {
this.mr.toggleMergeDetails(false);
}
});
},
getServiceEndpoints(store) {
return {
@ -581,9 +555,9 @@ export default {
</div>
<div class="mr-widget-section" data-testid="mr-widget-content">
<template v-if="mergeBlockedComponentEnabled">
<template v-if="mergeBlockedComponentVisible">
<mr-widget-auto-merge-enabled
v-if="mr.autoMergeEnabled"
v-if="autoMergeEnabled"
:mr="mr"
:service="service"
class="gl-border-b-1 gl-border-b-solid gl-border-gray-100"
@ -591,12 +565,7 @@ export default {
<merge-checks :mr="mr" :service="service" />
</template>
<component :is="componentName" v-else :mr="mr" :service="service" />
<ready-to-merge
v-if="mr.commitsCount"
v-show="shouldShowMergeDetails"
:mr="mr"
:service="service"
/>
<ready-to-merge v-if="mr.commitsCount" :mr="mr" :service="service" />
</div>
</div>
<mr-widget-pipeline-container

View File

@ -17,35 +17,14 @@ export default function deviseState() {
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
return stateKey.checking;
}
if (this.hasConflicts) {
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.conflicts;
}
if (this.shouldBeRebased) {
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.rebase;
}
if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.mergeChecksFailed;
}
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.pipelineFailed;
}
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.draft;
}
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.unresolvedDiscussions;
}
if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
}
if (this.autoMergeEnabled && !this.mergeError) {
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.autoMergeEnabled;
}
if (
this.detailedMergeStatus === DETAILED_MERGE_STATUS.MERGEABLE ||
this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_STILL_RUNNING
) {
return stateKey.readyToMerge;
}
return window.gon?.features?.mergeBlockedComponent ? null : stateKey.checking;
return null;
}

View File

@ -30,11 +30,10 @@ export default class MergeRequestStore {
this.stateMachine = machine(STATE_MACHINE.definition);
this.machineValue = this.stateMachine.value;
this.mergeDetailsCollapsed =
!window.gon?.features?.mergeBlockedComponent && window.innerWidth < 768;
this.mergeError = data.mergeError;
this.multipleApprovalRulesAvailable = data.multiple_approval_rules_available || false;
this.id = data.id;
this.autoMergeEnabled = false;
this.setPaths(data);
@ -420,12 +419,4 @@ export default class MergeRequestStore {
this.transitionStateMachine(transitionOptions);
}
toggleMergeDetails(val = !this.mergeDetailsCollapsed) {
if (window.gon?.features?.mergeBlockedComponent) {
return;
}
this.mergeDetailsCollapsed = val;
}
}

View File

@ -2,56 +2,27 @@ export const stateToComponentMap = {
merged: 'mr-widget-merged',
closed: 'mr-widget-closed',
merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch',
draft: 'mr-widget-wip',
readyToMerge: 'mr-widget-ready-to-merge',
nothingToMerge: 'mr-widget-nothing-to-merge',
notAllowedToMerge: 'mr-widget-not-allowed',
archived: 'mr-widget-archived',
checking: 'mr-widget-checking',
preparing: 'mr-widget-preparing',
unresolvedDiscussions: 'mr-widget-unresolved-discussions',
pipelineBlocked: 'mr-widget-pipeline-blocked',
pipelineFailed: 'mr-widget-pipeline-failed',
autoMergeEnabled: 'mr-widget-auto-merge-enabled',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'sha-mismatch',
rebase: 'mr-widget-rebase',
mergeChecksFailed: 'mergeChecksFailed',
};
export const statesToShowHelpWidget = [
'merging',
'conflicts',
'draft',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
'rebase',
];
export const stateKey = {
archived: 'archived',
missingBranch: 'missingBranch',
nothingToMerge: 'nothingToMerge',
preparing: 'preparing',
checking: 'checking',
conflicts: 'conflicts',
draft: 'draft',
pipelineFailed: 'pipelineFailed',
unresolvedDiscussions: 'unresolvedDiscussions',
pipelineBlocked: 'pipelineBlocked',
shaMismatch: 'shaMismatch',
autoMergeFailed: 'autoMergeFailed',
autoMergeEnabled: 'autoMergeEnabled',
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
rebase: 'rebase',
merging: 'merging',
merged: 'merged',
mergeChecksFailed: 'mergeChecksFailed',

View File

@ -43,7 +43,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
push_frontend_feature_flag(:merge_blocked_component, current_user)
push_frontend_feature_flag(:auto_merge_when_incomplete_pipeline_succeeds, project)
push_frontend_feature_flag(:pinned_file, project)
push_frontend_feature_flag(:reviewer_assign_drawer, current_user)

View File

@ -33,21 +33,20 @@ module Ci
def above_threshold?(threshold)
with_ci_connection do
Ci::Partitionable.registered_models.any? do |model|
database_partition = model.partitioning_strategy.partition_for_id(id)
database_partition && database_partition.data_size > threshold
end
Gitlab::Database::PostgresPartition
.with_parent_tables(parent_table_names)
.with_list_constraint(id)
.above_threshold(threshold)
.exists?
end
end
def all_partitions_exist?
with_ci_connection do
Ci::Partitionable.registered_models.all? do |model|
model
.partitioning_strategy
.partition_for_id(id)
.present?
end
Gitlab::Database::PostgresPartition
.with_parent_tables(parent_table_names)
.with_list_constraint(id)
.count == parent_table_names.size
end
end
@ -56,5 +55,9 @@ module Ci
def with_ci_connection(&block)
Gitlab::Database::SharedModel.using_connection(connection, &block)
end
def parent_table_names
Ci::Partitionable.registered_models.map(&:table_name)
end
end
end

View File

@ -10,8 +10,7 @@ module ServicePing
SubmissionError = Class.new(StandardError)
def initialize(skip_db_write: false, payload: nil)
@skip_db_write = skip_db_write
def initialize(payload: nil)
@payload = payload
end
@ -36,7 +35,7 @@ module ServicePing
private
attr_reader :payload, :skip_db_write
attr_reader :payload
def metadata(service_ping_payload)
{
@ -83,8 +82,6 @@ module ServicePing
raise SubmissionError, "Invalid usage_data_id in response: #{version_usage_data_id}"
end
return if skip_db_write
raw_usage_data = save_raw_usage_data(payload)
raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
ServicePing::DevopsReport.new(response).execute

View File

@ -1,4 +1,4 @@
.top-area
.gl-flex
= gl_tabs_nav({ class: 'gl-display-flex gl-flex-grow-1 gl-border-none'}) do
= gl_tab_link_to _('Most starred'), starred_explore_projects_path, { item_active: current_page?(starred_explore_projects_path) || current_page?(explore_root_path) }
= gl_tab_link_to _('Trending'), trending_explore_projects_path
@ -6,4 +6,6 @@
= gl_tab_link_to _('Inactive'), explore_projects_path({ archived: 'only' }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'only' }
= gl_tab_link_to _('All'), explore_projects_path({ archived: true }), { item_active: current_page?(explore_projects_path) && params['archived'] == 'true' }
#js-projects-explore-filtered-search-and-sort{ data: { app_data: projects_explore_filtered_search_and_sort_app_data } }
#js-projects-explore-filtered-search-and-sort.gl-py-5.gl-border-t.gl-border-b{ data: { app_data: projects_explore_filtered_search_and_sort_app_data } }
-# This element takes up space while Vue is rendering to avoid page jump
.gl-h-7

View File

@ -21,7 +21,6 @@ class GitlabServicePingWorker # rubocop:disable Scalability/IdempotentWorker
#
# See https://github.com/mperham/sidekiq/issues/2372
triggered_from_cron = options.fetch('triggered_from_cron', true)
skip_db_write = options.fetch('skip_db_write', false)
# Disable service ping for GitLab.com unless called manually
# See https://gitlab.com/gitlab-org/gitlab/-/issues/292929 for details
@ -32,7 +31,7 @@ class GitlabServicePingWorker # rubocop:disable Scalability/IdempotentWorker
# Splay the request over a minute to avoid thundering herd problems.
sleep(rand(0.0..60.0).round(3))
ServicePing::SubmitService.new(payload: usage_data, skip_db_write: skip_db_write).execute
ServicePing::SubmitService.new(payload: usage_data).execute
end
end

View File

@ -1,8 +0,0 @@
---
name: merge_blocked_component
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136454
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432033
milestone: '16.6'
type: development
group: group::code review
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: rate_limit_oauth_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133109
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427874
milestone: '16.5'
type: development
group: group::authentication
default_enabled: false

View File

@ -33,48 +33,6 @@
"instrumentation_class"
]
},
{
"properties": {
"instrumentation_class": {
"const": "AggregatedMetric"
},
"options": {
"type": "object",
"properties": {
"aggregate": {
"type": "object",
"properties": {
"attribute": {
"enum": [
"user.id",
"project.id"
]
}
},
"required": [
"attribute"
],
"additionalProperties": false
},
"events": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"aggregate",
"events"
],
"additionalProperties": false
}
},
"required": [
"instrumentation_class",
"options"
]
},
{
"properties": {
"key_path": {

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class DropIndexAlertManagementHttpIntegrationsOnProjectId < Gitlab::Database::Migration[2.2]
milestone '17.1'
disable_ddl_transaction!
TABLE_NAME = :alert_management_http_integrations
INDEX_NAME = :index_alert_management_http_integrations_on_project_id
COLUMN_NAMES = [:project_id]
def up
remove_concurrent_index_by_name(TABLE_NAME, INDEX_NAME)
end
def down
add_concurrent_index(TABLE_NAME, COLUMN_NAMES, name: INDEX_NAME)
end
end

View File

@ -0,0 +1 @@
e59b68e02cc30c3881e9904514b57301dc0bb47ec47a32941165e3f3760e2e61

View File

@ -24487,8 +24487,6 @@ CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_iid ON alert
CREATE INDEX index_alert_management_alerts_on_prometheus_alert_id ON alert_management_alerts USING btree (prometheus_alert_id) WHERE (prometheus_alert_id IS NOT NULL);
CREATE INDEX index_alert_management_http_integrations_on_project_id ON alert_management_http_integrations USING btree (project_id);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id ON alert_management_alert_user_mentions USING btree (alert_management_alert_id) WHERE (note_id IS NULL);
CREATE UNIQUE INDEX index_alert_user_mentions_on_alert_id_and_note_id ON alert_management_alert_user_mentions USING btree (alert_management_alert_id, note_id);

View File

@ -50,7 +50,10 @@ The following example shows a sequence of requests and responses between:
- The content delivery network.
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
sequenceDiagram
accTitle: Request and response flow
accDescr: Describes how requests and responses flow from the user, GitLab, and a CDN.
User->>GitLab: GET /project/-/archive/master.zip
GitLab->>User: 302 Found
Note over User,GitLab: Location: https://cdn.com/project/-/archive/master.zip?token=secure-user-token

View File

@ -111,10 +111,10 @@ Monitored events: i_code_review_user_create_mr
+-----------------------------------------------------------------------------+------------------------------+-----------------------+---------------+---------------+
| Key Path | Monitored Events | Instrumentation Class | Initial Value | Current Value |
+-----------------------------------------------------------------------------+------------------------------+-----------------------+---------------+---------------+
| counts_monthly.aggregated_metrics.code_review_category_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 13 | 14 |
| counts_monthly.aggregated_metrics.code_review_group_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 13 | 14 |
| counts_weekly.aggregated_metrics.code_review_category_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 0 | 1 |
| counts_weekly.aggregated_metrics.code_review_group_monthly_active_users | i_code_review_user_create_mr | AggregatedMetric | 0 | 1 |
| counts_monthly.aggregated_metrics.code_review_category_monthly_active_users | i_code_review_user_create_mr | RedisHLLMetric | 13 | 14 |
| counts_monthly.aggregated_metrics.code_review_group_monthly_active_users | i_code_review_user_create_mr | RedisHLLMetric | 13 | 14 |
| counts_weekly.aggregated_metrics.code_review_category_monthly_active_users | i_code_review_user_create_mr | RedisHLLMetric | 0 | 1 |
| counts_weekly.aggregated_metrics.code_review_group_monthly_active_users | i_code_review_user_create_mr | RedisHLLMetric | 0 | 1 |
| redis_hll_counters.code_review.i_code_review_user_create_mr_monthly | i_code_review_user_create_mr | RedisHLLMetric | 8 | 9 |
| redis_hll_counters.code_review.i_code_review_user_create_mr_weekly | i_code_review_user_create_mr | RedisHLLMetric | 0 | 1 |
+-----------------------------------------------------------------------------+------------------------------+-----------------------+---------------+---------------+

View File

@ -292,7 +292,6 @@ To declare an aggregate of metrics based on events collected from database, foll
these steps:
1. [Persist the metrics for aggregation](#persist-metrics-for-aggregation).
1. [Add new aggregated metric definition](#add-new-aggregated-metric-definition).
#### Persist metrics for aggregation
@ -337,51 +336,6 @@ class UsageData
end
```
#### Add new aggregated metric definition
After all metrics are persisted, you can add an aggregated metric definition.
To declare the aggregate of metrics collected with [Estimated Batch Counters](#estimated-batch-counters),
you must fulfill the following requirements:
- Metrics names listed in the `events:` attribute, have to use the same names you passed in the `metric_name` argument while persisting metrics in previous step.
- Every metric listed in the `events:` attribute, has to be persisted for **every** selected `time_frame:` value.
### Availability-restrained Aggregated metrics
If the Aggregated metric should only be available in the report under specific conditions, then you must specify these conditions in a new class that is a child of the `AggregatedMetric` class.
```ruby
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class MergeUsageCountAggregatedMetric < AggregatedMetric
available? { Feature.enabled?(:merge_usage_data_missing_key_paths) }
end
end
end
end
end
```
You must also use the class's name in the YAML setup.
```yaml
time_frame: 28d
instrumentation_class: MergeUsageCountAggregatedMetric
data_source: redis_hll
options:
aggregate:
attribute: user.id
events:
- `incident_management_alert_status_changed`
- `incident_management_alert_assigned`
- `incident_management_alert_todo`
- `incident_management_alert_create_incident`
```
## Numbers metrics
- `operation`: Operations for the given `data` block. Currently we only support `add` operation.

View File

@ -45,10 +45,8 @@ with the _upstream_ project after merge.
- The Git LFS original v1 API is unsupported.
- Even when Git communicates with the repository over SSH, Git LFS objects still use HTTPS.
- Git LFS requests use HTTPS credentials, which means:
- You should use a good Git [credentials store](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
- If your GitLab server uses HTTP instead, you must
[add the URL to Git configuration manually](troubleshooting.md#getsockopt-connection-refused).
- Git LFS requests use HTTPS credentials, which means you should use a good Git
[credentials store](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
- [Group wikis](../../../user/project/wiki/group.md) do not support Git LFS.
## Add a file with Git LFS

View File

@ -8,14 +8,126 @@ info: "To determine the technical writer assigned to the Stage/Group associated
When working with Git LFS, you might encounter the following issues.
## Error: repository or object not found
This error can occur for a few reasons, including:
- **You don't have permissions to access certain LFS object.** Confirm you have
permission to push to the project, or fetch from the project.
- **The project isn't allowed to access the LFS object.** The LFS object you want
to push (or fetch) is no longer available to the project. In most cases, the object
has been removed from the server.
- **The local Git repository is using deprecated version of the Git LFS API.** Update
your local copy of Git LFS and try again.
## Invalid status for `<url>` : 501
Git LFS logs the failures into a log file. To view this log file:
1. In your terminal window, go to your project's directory.
1. Run this command to see recent log files:
```shell
git lfs logs last
```
These problems can cause `501` errors:
- Git LFS is not enabled in your project's settings. Check your project settings and
enable Git LFS.
- Git LFS support is not enabled on the GitLab server. Check with your GitLab
administrator why Git LFS is not enabled on the server. See
[LFS administration documentation](../../../administration/lfs/index.md) for instructions
on how to enable Git LFS support.
- The Git LFS client version is not supported by GitLab server. You should:
1. Check your Git LFS version with `git lfs version`.
1. Check the Git configuration of your project for traces of the deprecated API
with `git lfs -l`. If your configuration sets `batch = false`,
remove the line, then update your Git LFS client. GitLab supports only
versions 1.0.1 and newer.
## Credentials are always required when pushing an object
Git LFS authenticates the user with HTTP Basic Authentication on every push for
every object, so it requires user HTTPS credentials. By default, Git supports
remembering the credentials for each repository you use. For more information, see
the [official Git documentation](https://git-scm.com/docs/gitcredentials).
For example, you can tell Git to remember your password for a period of time in
which you expect to push objects. This example remembers your credentials for an hour
(3600 seconds), and you must authenticate again in an hour:
```shell
git config --global credential.helper 'cache --timeout=3600'
```
To store and encrypt credentials, see:
- MacOS: use `osxkeychain`.
- Windows: use `wincred` or Microsoft's
[Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
To learn more about storing your user credentials, see the
[Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
## LFS objects are missing on push
GitLab checks files on push to detect LFS pointers. If it detects LFS pointers,
GitLab tries to verify that those files already exist in LFS. If you use a separate
server for Git LFS, and you encounter this problem:
1. Verify you have installed Git LFS locally.
1. Consider a manual push with `git lfs push --all`.
If you store Git LFS files outside of GitLab, you can
[disable Git LFS](index.md#enable-or-disable-git-lfs-for-a-project) on your project.
## Hosting LFS objects externally
You can host LFS objects externally by setting a custom LFS URL:
```shell
git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs
```
You might do this if you store LFS data on an appliance, like a Nexus Repository.
If you use an external LFS store, GitLab can't verify the LFS objects. Pushes then
fail if you have GitLab LFS support enabled.
To stop push failures, you can disable Git LFS support in your
[Project settings](index.md#enable-or-disable-git-lfs-for-a-project). However, this approach
might not be desirable, because it also disables GitLab LFS features like:
- Verifying LFS objects.
- GitLab UI integration for LFS.
## I/O timeout when pushing LFS objects
If your network conditions are unstable, the Git LFS client might time out when trying to upload files.
You might see errors like:
```shell
LFS: Put "http://example.com/root/project.git/gitlab-lfs/objects/<OBJECT-ID>/15":
read tcp your-instance-ip:54544->your-instance-ip:443: i/o timeout
error: failed to push some refs to 'ssh://example.com:2222/root/project.git'
```
To fix this problem, set the client activity timeout a higher value. For example,
to set the timeout to 60 seconds:
```shell
git config lfs.activitytimeout 60
```
## Encountered `n` files that should have been pointers, but weren't
This error indicates the files are expected to be tracked by LFS, but
the repository is not tracking them as LFS. This issue can be one
potential reason for this error:
[Files not tracked with LFS when uploaded through the web interface](https://gitlab.com/gitlab-org/gitlab/-/issues/326342#note_586820485)
This error indicates the repository should be tracking a file with Git LFS, but
isn't. [Issue 326342](https://gitlab.com/gitlab-org/gitlab/-/issues/326342#note_586820485),
fixed in GitLab 16.10, was one cause of this problem.
To resolve the problem, migrate the affected file (or files) and push back to the repository:
To fix the problem, migrate the affected files, and push them up to the repository:
1. Migrate the file to LFS:
@ -35,128 +147,3 @@ To resolve the problem, migrate the affected file (or files) and push back to th
git reflog expire --expire-unreachable=now --all
git gc --prune=now
```
## error: Repository or object not found
This error can occur for a few reasons, including:
- You don't have permissions to access certain LFS object
Check if you have permissions to push to the project or fetch from the project.
- Project is not allowed to access the LFS object
LFS object you are trying to push to the project or fetch from the project is not
available to the project anymore. Probably the object was removed from the server.
- Local Git repository is using deprecated LFS API
## Invalid status for `<url>` : 501
Git LFS logs the failures into a log file.
To view this log file, while in project directory:
```shell
git lfs logs last
```
If the status `error 501` is shown, it is because:
- Git LFS is not enabled in project settings. Check your project settings and
enable Git LFS.
- Git LFS support is not enabled on the GitLab server. Check with your GitLab
administrator why Git LFS is not enabled on the server. See
[LFS administration documentation](../../../administration/lfs/index.md) for instructions
on how to enable LFS support.
- Git LFS client version is not supported by GitLab server. Check your Git LFS
version with `git lfs version`. Check the Git configuration of the project for traces
of deprecated API with `git lfs -l`. If `batch = false` is set in the configuration,
remove the line and try to update your Git LFS client. Only version 1.0.1 and
newer are supported.
## `getsockopt: connection refused`
If you push an LFS object to a project and receive an error like this,
the LFS client is trying to reach GitLab through HTTPS. However, your GitLab
instance is being served on HTTP:
```plaintext
Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused
```
This behavior is caused by Git LFS using HTTPS connections by default when a
`lfsurl` is not set in the Git configuration.
To prevent this from happening, set the LFS URL in project Git configuration:
```shell
git config --add lfs.url "http://gitlab.example.com/group/my-sample-project.git/info/lfs"
```
## Credentials are always required when pushing an object
NOTE:
With 8.12 GitLab added LFS support to SSH. The Git LFS communication
still goes over HTTP, but now the SSH client passes the correct credentials
to the Git LFS client. No action is required by the user.
Git LFS authenticates the user with HTTP Basic Authentication on every push for
every object, so user HTTPS credentials are required.
By default, Git has support for remembering the credentials for each repository
you use. For more information, see the [official Git documentation](https://git-scm.com/docs/gitcredentials).
For example, you can tell Git to remember the password for a period of time in
which you expect to push the objects:
```shell
git config --global credential.helper 'cache --timeout=3600'
```
This remembers the credentials for an hour, after which Git operations
require re-authentication.
If you are using OS X you can use `osxkeychain` to store and encrypt your credentials.
For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
More details about various methods of storing the user credentials can be found
on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
## LFS objects are missing on push
GitLab checks files to detect LFS pointers on push. If LFS pointers are detected, GitLab tries to verify that those files already exist in LFS on GitLab.
Verify that LFS is installed locally and consider a manual push with `git lfs push --all`.
If you are storing LFS files outside of GitLab you can disable LFS on the project by setting `lfs_enabled: false` with the [projects API](../../../api/projects.md#edit-project).
## Hosting LFS objects externally
It is possible to host LFS objects externally by setting a custom LFS URL with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
You might choose to do this if you are using an appliance like a Nexus Repository to store LFS data. If you choose to use an external LFS store,
GitLab can't verify LFS objects. Pushes then fail if you have GitLab LFS support enabled.
To stop push failure, LFS support can be disabled in the [Project settings](index.md#enable-or-disable-git-lfs-for-a-project), which also disables GitLab LFS value-adds (Verifying LFS objects, UI integration for LFS).
## I/O timeout when pushing LFS objects
You might get an error that states:
```shell
LFS: Put "http://your-instance.com/root/project.git/gitlab-lfs/objects/cc29e205d04a4062d0fb131700e8bfc8e54c44d0176a8dca22f40b24ef26d325/15": read tcp your-instance-ip:54544->your-instance-ip:443: i/o timeout
error: failed to push some refs to 'ssh://your-instance.com:2222/root/project.git'
```
When network conditions are unstable, the Git LFS client might time out when trying to upload files
if network conditions are unstable.
The workaround is to set the client activity timeout a higher value.
For example, to set the timeout to 60 seconds:
```shell
git config lfs.activitytimeout 60
```

View File

@ -22,9 +22,9 @@ DETAILS:
To use this feature:
- The parent group of the project must:
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- Enable the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features).
- You must:
- Belong to at least one group with the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features) enabled.
- Belong to at least one group with the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features) enabled.
- Have sufficient permissions to view the project.
GitLab can help you get up to speed faster if you:
@ -74,9 +74,9 @@ DETAILS:
To use this feature:
- The parent group of the issue must:
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- Enable the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features).
- You must:
- Belong to at least one group with the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features) enabled.
- Belong to at least one group with the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features) enabled.
- Have sufficient permissions to view the issue.
You can generate a summary of discussions on an issue:
@ -104,9 +104,9 @@ DETAILS:
To use this feature:
- The parent group of the project must:
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- Enable the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features).
- You must:
- Belong to at least one group with the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features) enabled.
- Belong to at least one group with the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features) enabled.
- Have sufficient permissions to view the CI/CD analytics.
In CI/CD Analytics, you can view a forecast of deployment frequency:
@ -136,9 +136,9 @@ DETAILS:
To use this feature:
- The parent group of the project must:
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- Enable the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features).
- You must:
- Belong to at least one group with the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features) enabled.
- Belong to at least one group with the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features) enabled.
- Have sufficient permissions to view the CI/CD job.
When the feature is available, the "Root cause analysis" button will appears on
@ -157,9 +157,9 @@ DETAILS:
To use this feature:
- The parent group of the project must:
- Enable the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features).
- Enable the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features).
- You must:
- Belong to at least one group with the [experiment and beta features setting](group/manage.md#enable-experiment-and-beta-features) enabled.
- Belong to at least one group with the [experiment and beta features setting](ai_features_enable.md#turn-on-beta-and-experimental-features) enabled.
- Have sufficient permissions to view the issue.
You can generate the description for an issue from a short summary.

View File

@ -134,19 +134,37 @@ to override the setting for specific groups or projects.
## Turn on Beta and Experimental features
Features listed as Experiment and Beta are turned off by default.
GitLab Duo features that are Experiment and Beta are turned off by default.
These features are subject to the [Testing Agreement](https://handbook.gitlab.com/handbook/legal/testing-agreement/).
### On GitLab.com
You can turn on Experiment and Beta features for your group on GitLab.com.
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222) in GitLab 16.0.
> - [Added to GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147833) in GitLab 16.11.
You can turn on GitLab Duo Experiment and Beta features for your group on GitLab.com.
Prerequisites:
- You must have the Owner role in the top-level group.
To turn on Beta and Experimental GitLab Duo features, use the [Experiment and Beta features checkbox](group/manage.md#enable-experiment-and-beta-features).
To turn on GitLab Duo Experiment and Beta features for a top-level group:
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > General**.
1. Expand **Permissions and group features**.
1. Under **Experiment and Beta features**, select the **Use Experiment and Beta features** checkbox.
1. Select **Save changes**.
This setting [cascades to all projects](../user/project/merge_requests/approvals/settings.md#cascade-settings-from-the-instance-or-top-level-group)
that belong to the group.
### On self-managed
To enable Beta and Experimental GitLab Duo features for GitLab versions where GitLab Duo Chat is not yet generally available, see the [GitLab Duo Chat documentation](gitlab_duo_chat_enable.md#for-self-managed).
To enable GitLab Duo Beta and Experimental features for GitLab versions
where GitLab Duo Chat is not yet generally available, see the
[GitLab Duo Chat documentation](gitlab_duo_chat_enable.md#for-self-managed).

View File

@ -299,7 +299,8 @@ DETAILS:
Prerequisites:
- The parent group of the project must have [experiment and beta features enabled](../group/manage.md#enable-experiment-and-beta-features).
- The top-level group of the project must have GitLab Duo
[experiment and beta features enabled](../ai_features_enable.md#turn-on-beta-and-experimental-features).
To generate a custom visualization with GitLab Duo using a natural language query:

View File

@ -426,32 +426,6 @@ Approval settings should not be confused with [approval rules](../project/merge_
for the ability to set merge request approval rules for groups is tracked in
[epic 4367](https://gitlab.com/groups/gitlab-org/-/epics/4367).
## Enable Experiment and Beta features
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118222) in GitLab 16.0.
> - [Added to GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147833) in GitLab 16.11.
WARNING:
[Experiment and Beta features](../../policy/experiment-beta-support.md) may produce unexpected results
(for example, the results might be low-quality, incomplete, incoherent, offensive, or insensitive,
and might include insecure code or failed pipelines).
You can give all users in a top-level group access to Experiment and Beta features.
This setting [cascades to all projects](../project/merge_requests/approvals/settings.md#cascade-settings-from-the-instance-or-top-level-group)
that belong to the group.
To enable Experiment features for a top-level group:
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > General**.
1. Expand **Permissions and group features**.
1. Under **Experiment and Beta features**, select the **Use Experiment and Beta features** checkbox.
1. Select **Save changes**.
## Group activity analytics
DETAILS:

View File

@ -359,6 +359,8 @@ commit C has no other trailer, only commit A is added to the changelog:
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
graph LR
accTitle: Flowchart of 3 commits
accDescr: Shows the flow of 3 commits, where commit C reverts commit B, but it contains no trailer
A[Commit A<br>Changelog: changed] --> B[Commit B<br>Changelog: changed]
B --> C[Commit C<br>Reverts commit B]
```
@ -369,6 +371,8 @@ both commits A and C are included in the changelog:
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
graph LR
accTitle: Flowchart of 3 commits
accDescr: Shows the flow of 3 commits, where commit C reverts commit B, but both commits A and C contain trailers
A[Commit A<br><br>Changelog: changed] --> B[Commit B<br><br>Changelog: changed]
B --> C[Commit C<br>Reverts commit B<br>Changelog: changed]
```

View File

@ -116,6 +116,8 @@ file.md @group-x @group-x/subgroup-y
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
graph TD
accTitle: Diagram of group inheritance
accDescr: If a subgroup owns a project, the parent group inherits ownership.
A[Parent group X] -->|owns| B[Project A]
A -->|contains| C[Subgroup Y]
C -->|owns| D[Project B]
@ -141,6 +143,8 @@ so that their members also become eligible Code Owners.
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
graph LR
accTitle: Diagram of subgroup inheritance
accDescr: Inviting a subgroup directly to a project affects whether their approvals can be made required.
A[Parent group X] -->|owns| B[Project A]
A -->|also contains| C[Subgroup Y]
C -.->D{Invite Subgroup Y<br/>to Project A?} -.->|yes| E[Members of Subgroup Y<br/>can submit Approvals]

View File

@ -20,6 +20,8 @@ The examples on this page assume a `main` branch with commits A, C, and E, and a
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
gitGraph
accTitle: Diagram of a merge
accDescr: A Git graph of five commits on two branches, which will be expanded on in other graphs in this page.
commit id: "A"
branch feature
commit id: "B"
@ -60,6 +62,8 @@ The merge strategy:
```mermaid
%%{init: { 'gitGraph': {'logLevel': 'debug', 'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'main', 'fontFamily': 'GitLab Sans'}} }%%
gitGraph
accTitle: Diagram of a merge commit
accDescr: A Git graph showing how merge commits are created in GitLab when a feature branch is merged.
commit id: "A"
branch feature
commit id: "B"
@ -76,6 +80,8 @@ looks like this:
```mermaid
%%{init: { 'gitGraph': {'logLevel': 'debug', 'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'main', 'fontFamily': 'GitLab Sans'}} }%%
gitGraph
accTitle: Diagram of the Merge Commit method
accDescr: A Git graph showing the structure of a Git repository after a feature branch is merged.
commit id: "A"
commit id: "C"
commit id: "E"
@ -90,6 +96,8 @@ on the `feature` branch, and the squash commit is placed on the `main` branch:
```mermaid
%%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'main', 'fontFamily': 'GitLab Sans'}} }%%
gitGraph
accTitle: Diagram of of a squash merge
accDescr: A Git graph showing repository and branch structure after a squash commit is added to the main branch.
commit id:"A"
branch feature
checkout main
@ -129,6 +137,8 @@ commit graph generated using this merge method:
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
gitGraph
accTitle: Diagram of a merge commit
accDescr: Shows the flow of commits when a branch merges with a merge commit.
commit id: "Init"
branch mr-branch-1
commit
@ -169,6 +179,8 @@ generated using this merge method:
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
gitGraph
accTitle: Diagram of a fast-forward merge
accDescr: Shows how a fast-forwarded merge request maintains a linear Git history, but does not add a merge commit.
commit id: "Init"
commit id: "Merge mr-branch-1"
commit id: "Merge mr-branch-2"

View File

@ -460,6 +460,8 @@ on branch `A`:
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
gitGraph
accTitle: Diagram of multiple branches with the same commit
accDescr: Branches A and B contain the same commit, but branch B also contains other commits. Merging branch B makes branch A appear as merged, because all its commits are merged.
commit id:"a"
branch "branch A"
commit id:"b"

View File

@ -12,9 +12,9 @@ module Gitlab
def perform
each_sub_batch do |batch|
batch.update_all('monitor_access_level=operations_access_level,' \
'infrastructure_access_level=operations_access_level,' \
' feature_flags_access_level=operations_access_level,'\
' environments_access_level=operations_access_level')
'infrastructure_access_level=operations_access_level, ' \
'feature_flags_access_level=operations_access_level, '\
'environments_access_level=operations_access_level')
end
end

View File

@ -111,16 +111,16 @@ module Gitlab
def add_schema_version_errors
if report_version.nil?
template = _("Report version not provided,"\
" %{report_type} report type supports versions: %{supported_schema_versions}."\
" GitLab will attempt to validate this report against the earliest supported versions of this report"\
" type, to show all the errors but will not ingest the report")
template = _("Report version not provided, "\
"%{report_type} report type supports versions: %{supported_schema_versions}. "\
"GitLab will attempt to validate this report against the earliest supported versions of this report "\
"type, to show all the errors but will not ingest the report")
message = format(template, report_type: report_type, supported_schema_versions: supported_schema_versions)
else
template = _("Version %{report_version} for report type %{report_type} is unsupported, supported versions"\
" for this report type are: %{supported_schema_versions}."\
" GitLab will attempt to validate this report against the earliest supported versions of this report"\
" type, to show all the errors but will not ingest the report")
template = _("Version %{report_version} for report type %{report_type} is unsupported, supported versions "\
"for this report type are: %{supported_schema_versions}. "\
"GitLab will attempt to validate this report against the earliest supported versions of this report "\
"type, to show all the errors but will not ingest the report")
message = format(template, report_version: report_version, report_type: report_type, supported_schema_versions: supported_schema_versions)
end
@ -165,9 +165,9 @@ module Gitlab
end
def add_supported_major_minor_behavior_warning
template = _("This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\
" any vendored schema version. Validation will be attempted against version"\
" %{find_latest_patch_version}")
template = _("This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match "\
"any vendored schema version. Validation will be attempted against version "\
"%{find_latest_patch_version}")
message = format(template, find_latest_patch_version: find_latest_patch_version)

View File

@ -20,8 +20,8 @@ module Gitlab
if use_primary?(strategy)
::Gitlab::Database::LoadBalancing::Session.current.use_primary!
elsif strategy == :retry
raise JobReplicaNotUpToDate, "Sidekiq job #{resolved_class} JID-#{job['jid']} couldn't use the replica."\
" Replica was not up to date."
raise JobReplicaNotUpToDate, "Sidekiq job #{resolved_class} JID-#{job['jid']} couldn't use the replica. "\
"Replica was not up to date."
else
# this means we selected an up-to-date replica, but there is nothing to do in this case.
end

View File

@ -38,12 +38,6 @@ module Gitlab
super || initial_partition
end
def partition_for_id(partition_id)
current_partitions.find do |partition|
partition_id.in?(partition.values)
end
end
private
def desired_partitions

View File

@ -33,8 +33,8 @@ module Gitlab
partitioned_table = find_partitioned_table(table_name)
if index_name_exists?(table_name, options[:name])
Gitlab::AppLogger.warn "Index not created because it already exists (this may be due to an aborted" \
" migration or similar): table_name: #{table_name}, index_name: #{options[:name]}"
Gitlab::AppLogger.warn "Index not created because it already exists (this may be due to an aborted " \
"migration or similar): table_name: #{table_name}, index_name: #{options[:name]}"
return
end

View File

@ -389,8 +389,8 @@ module Gitlab
def create_range_id_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_keys)
if table_exists?(partitioned_table_name)
Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \
" (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} "
Gitlab::AppLogger.warn "Partitioned table not created because it already exists " \
"(this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} "
return
end
@ -417,8 +417,8 @@ module Gitlab
def create_range_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_key)
if table_exists?(partitioned_table_name)
Gitlab::AppLogger.warn "Partitioned table not created because it already exists" \
" (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} "
Gitlab::AppLogger.warn "Partitioned table not created because it already exists " \
"(this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} "
return
end
@ -485,8 +485,8 @@ module Gitlab
def create_range_partition_safely(partition_name, table_name, lower_bound, upper_bound)
if table_exists?(table_for_range_partition(partition_name))
Gitlab::AppLogger.warn "Partition not created because it already exists" \
" (this may be due to an aborted migration or similar): partition_name: #{partition_name}"
Gitlab::AppLogger.warn "Partition not created because it already exists " \
"(this may be due to an aborted migration or similar): partition_name: #{partition_name}"
return
end
@ -503,8 +503,8 @@ module Gitlab
def create_sync_function(name, partitioned_table_name, unique_key)
if function_exists?(name)
Gitlab::AppLogger.warn "Partitioning sync function not created because it already exists" \
" (this may be due to an aborted migration or similar): function name: #{name}"
Gitlab::AppLogger.warn "Partitioning sync function not created because it already exists " \
"(this may be due to an aborted migration or similar): function name: #{name}"
return
end
@ -549,8 +549,8 @@ module Gitlab
def create_sync_trigger(table_name, trigger_name, function_name)
if trigger_exists?(table_name, trigger_name)
Gitlab::AppLogger.warn "Partitioning sync trigger not created because it already exists" \
" (this may be due to an aborted migration or similar): trigger name: #{trigger_name}"
Gitlab::AppLogger.warn "Partitioning sync trigger not created because it already exists " \
"(this may be due to an aborted migration or similar): trigger name: #{trigger_name}"
return
end

View File

@ -29,6 +29,20 @@ module Gitlab
end
end
scope :with_parent_tables, ->(parent_tables) do
parent_identifiers = parent_tables.map { |name| "#{connection.current_schema}.#{name}" }
where(parent_identifier: parent_identifiers).order(:name)
end
scope :with_list_constraint, ->(condition) do
where(sanitize_sql_for_conditions(['condition LIKE ?', "FOR VALUES IN (%'#{condition.to_i}'%)"]))
end
scope :above_threshold, ->(threshold) do
where('pg_table_size(identifier) > ?', threshold)
end
def self.partition_exists?(table_name)
where("identifier = concat(current_schema(), '.', ?)", table_name).exists?
end

View File

@ -41,11 +41,11 @@ module Gitlab
requested_reviewer = User.find(requested_reviewer_id).to_reference
if issue_event.event == 'review_request_removed'
"#{SystemNotes::IssuablesService.issuable_events[:review_request_removed]}" \
" #{requested_reviewer}"
"#{SystemNotes::IssuablesService.issuable_events[:review_request_removed]} " \
"#{requested_reviewer}"
else
"#{SystemNotes::IssuablesService.issuable_events[:review_requested]}" \
" #{requested_reviewer}"
"#{SystemNotes::IssuablesService.issuable_events[:review_requested]} " \
"#{requested_reviewer}"
end
end
end

View File

@ -72,9 +72,9 @@ module Gitlab
end
def show_import_start_message
logger.info "Importing GitLab export: #{file_path} into GitLab" \
" #{full_path}" \
" as #{current_user.name}"
logger.info "Importing GitLab export: #{file_path} into GitLab " \
"#{full_path} " \
"as #{current_user.name}"
end
def import_params

View File

@ -33,11 +33,7 @@ module Gitlab
end
def api_request?
if ::Feature.enabled?(:rate_limit_oauth_api, ::Feature.current_request)
matches?(API_PATH_REGEX)
else
logical_path.start_with?('/api')
end
matches?(API_PATH_REGEX)
end
def logical_path

View File

@ -51,8 +51,8 @@ module Gitlab
redis_cmd do |redis|
current_value = redis.decr(key)
if current_value < 0
Gitlab::AppLogger.warn("Reference counter for #{gl_repository} decreased" \
" when its value was less than 1. Resetting the counter.")
Gitlab::AppLogger.warn("Reference counter for #{gl_repository} decreased " \
"when its value was less than 1. Resetting the counter.")
redis.del(key)
end
end

View File

@ -64,8 +64,8 @@ module Gitlab
def check_argument_type(argument_name, argument_value, allowed_classes)
return if argument_value.nil? || allowed_classes.any? { |allowed_class| argument_value.is_a?(allowed_class) }
exception = "Invalid argument type passed for #{argument_name}." \
" Should be one of #{allowed_classes.map(&:to_s)}"
exception = "Invalid argument type passed for #{argument_name}. " \
"Should be one of #{allowed_classes.map(&:to_s)}"
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new(exception))
end

View File

@ -10,15 +10,9 @@ module Gitlab
UndefinedEvents = Class.new(AggregatedMetricError)
DATABASE_SOURCE = 'database'
REDIS_SOURCE = 'redis_hll'
INTERNAL_EVENTS_SOURCE = 'internal_events'
SOURCES = {
DATABASE_SOURCE => Sources::PostgresHll,
REDIS_SOURCE => Sources::RedisHll,
# Same strategy as RedisHLL, since they are a part of internal events
# and should get counted together with other RedisHLL-based aggregations
INTERNAL_EVENTS_SOURCE => Sources::RedisHll
DATABASE_SOURCE => Sources::PostgresHll
}.freeze
end
end

View File

@ -1,82 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Aggregates
class Aggregate
include Gitlab::Usage::TimeFrame
def initialize(recorded_at)
@recorded_at = recorded_at
end
def calculate_count_for_aggregation(aggregation:, time_frame:)
with_validate_configuration(aggregation, time_frame) do
source = SOURCES[aggregation[:source]]
events = select_defined_events(aggregation[:events], aggregation[:source])
property_name = aggregation[:attribute]
source.calculate_metrics_union(**time_constraints(time_frame)
.merge(metric_names: events, property_name: property_name, recorded_at: recorded_at))
end
rescue Gitlab::UsageDataCounters::HLLRedisCounter::EventError, AggregatedMetricError => error
failure(error)
end
private
attr_accessor :recorded_at
def with_validate_configuration(aggregation, time_frame)
source = aggregation[:source]
unless SOURCES[source]
return failure(
UnknownAggregationSource
.new("Aggregation source: '#{source}' must be included in #{SOURCES.keys}")
)
end
if time_frame == Gitlab::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME && source == REDIS_SOURCE
return failure(
DisallowedAggregationTimeFrame
.new("Aggregation time frame: 'all' is not allowed for aggregation with source: '#{REDIS_SOURCE}'")
)
end
yield
end
def failure(error)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(error)
Gitlab::Utils::UsageData::FALLBACK
end
def time_constraints(time_frame)
case time_frame
when Gitlab::Usage::TimeFrame::TWENTY_EIGHT_DAYS_TIME_FRAME_NAME
monthly_time_range
when Gitlab::Usage::TimeFrame::SEVEN_DAYS_TIME_FRAME_NAME
weekly_time_range
when Gitlab::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME
{ start_date: nil, end_date: nil }
end
end
def select_defined_events(events, source)
# Database source metrics get validated inside the PostgresHll class:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb#L16
return events if source != ::Gitlab::Usage::Metrics::Aggregates::REDIS_SOURCE
events.select do |event|
::Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event)
end
end
end
end
end
end
end
Gitlab::Usage::Metrics::Aggregates::Aggregate.prepend_mod

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Aggregates
module Sources
class RedisHll
def self.calculate_metrics_union(metric_names:, start_date:, end_date:, property_name:, recorded_at: nil)
union = Gitlab::UsageDataCounters::HLLRedisCounter.calculate_events_union(
event_names: metric_names,
property_name: property_name,
start_date: start_date,
end_date: end_date
)
return union if union >= 0
raise UnionNotAvailable, "Union data not available for #{metric_names}"
end
end
end
end
end
end
end

View File

@ -1,57 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
# Usage example
#
# In metric YAML definition:
#
# instrumentation_class: AggregatedMetric
# data_source: redis_hll
# options:
# aggregate:
# attribute: user.id
# events:
# - 'incident_management_alert_status_changed'
# - 'incident_management_alert_assigned'
# - 'incident_management_alert_todo'
# - 'incident_management_alert_create_incident'
class AggregatedMetric < BaseMetric
FALLBACK = -1
def initialize(metric_definition)
super
@source = metric_definition[:data_source]
@aggregate = options.fetch(:aggregate, {})
end
def value
alt_usage_data(fallback: FALLBACK) do
Aggregates::Aggregate
.new(Time.current)
.calculate_count_for_aggregation(
aggregation: aggregate_config,
time_frame: time_frame
)
end
end
private
attr_accessor :source, :aggregate
def aggregate_config
{
source: source,
events: options[:events],
attribute: aggregate[:attribute]
}
end
end
end
end
end
end

View File

@ -61690,48 +61690,15 @@ msgstr ""
msgid "mrWidget|%{boldHeaderStart}Looks like there's no pipeline here.%{boldHeaderEnd}"
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} Select %{boldStart}Mark as ready%{boldEnd} to remove it from Draft status."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} Users who can write to the source or target branches can resolve the conflicts."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} a Jira issue key must be mentioned in the title or description."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} all required approvals must be given."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} all status checks must pass."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} all threads must be resolved."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} denied licenses must be removed."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} fast-forward merge is not possible. To merge this request, first rebase locally."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} merge conflicts must be resolved."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} new changes were just added."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. It's waiting for a manual action to continue."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. Push a commit that fixes the failure or %{linkStart}learn about other solutions.%{linkEnd}"
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} the source branch must be rebased onto the target branch."
msgstr ""
msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} you can only merge after the above items are resolved."
msgstr ""
msgid "mrWidget|%{boldStart}Merge unavailable:%{boldEnd} merge requests are read-only in a secondary Geo node."
msgstr ""
@ -61765,9 +61732,6 @@ msgstr ""
msgid "mrWidget|%{boldStart}Merging!%{boldEnd} We're almost there…"
msgstr ""
msgid "mrWidget|%{boldStart}Ready to be merged automatically.%{boldEnd} Ask someone with write access to this repository to merge this request."
msgstr ""
msgid "mrWidget|%{dangerStart}%{rules} rule can't be approved%{dangerEnd}"
msgstr ""
@ -61911,9 +61875,6 @@ msgstr ""
msgid "mrWidget|Loading deployment statistics"
msgstr ""
msgid "mrWidget|Mark as ready"
msgstr ""
msgid "mrWidget|Members who can merge are allowed to add commits."
msgstr ""
@ -61943,9 +61904,6 @@ msgstr ""
msgid "mrWidget|Rebase"
msgstr ""
msgid "mrWidget|Rebase in progress"
msgstr ""
msgid "mrWidget|Rebase without pipeline"
msgstr ""
@ -61961,12 +61919,6 @@ msgstr ""
msgid "mrWidget|Remove from merge train"
msgstr ""
msgid "mrWidget|Resolve conflicts"
msgstr ""
msgid "mrWidget|Resolve locally"
msgstr ""
msgid "mrWidget|Revert"
msgstr ""

View File

@ -71,9 +71,8 @@ module QA
element 'revert-button'
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do
view 'app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue' do
element 'standard-rebase-button'
element 'rebase-message'
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue' do
@ -224,22 +223,6 @@ module QA
click_by_javascript(find_element('edit-title-button', skip_finished_loading_check: true))
end
def fast_forward_not_possible?
has_element?('rebase-message')
end
def merge_blocked_component_ff_enabled?
element = within_element('.mr-widget-section') do
feature_flag_controlled_element(
:merge_blocked_component,
'chevron-lg-down-icon',
'standard-rebase-button'
)
end
!(element == 'standard-rebase-button')
end
def expand_merge_checks
within_element('.mr-widget-section') do
click_element('chevron-lg-down-icon')

View File

@ -23,15 +23,9 @@ module QA
merge_request.visit!
Page::MergeRequest::Show.perform do |mr_page|
if mr_page.merge_blocked_component_ff_enabled?
expect(mr_page).to have_content('Merge blocked: 1 check failed', wait: 20)
mr_page.expand_merge_checks
expect(mr_page).to have_content('Merge request must be rebased, because a fast-forward merge is not possible.')
else
expect(mr_page).to have_content('Merge blocked: the source branch must be rebased onto the target branch.', wait: 20)
expect(mr_page).to be_fast_forward_not_possible
page.refresh
end
expect(mr_page).to have_content('Merge blocked: 1 check failed', wait: 20)
mr_page.expand_merge_checks
expect(mr_page).to have_content('Merge request must be rebased, because a fast-forward merge is not possible.')
expect(mr_page).not_to have_merge_button
expect(merge_request.project.commits.size).to eq(2)

View File

@ -53,10 +53,10 @@ module RuboCop
MSG_STYLE_GUIDE_LINK = 'See the description style guide: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#description-style-guide'
MSG_NO_DESCRIPTION = "Please add a `description` property. #{MSG_STYLE_GUIDE_LINK}".freeze
MSG_NO_PERIOD = "`description` strings must end with a `.`. #{MSG_STYLE_GUIDE_LINK}".freeze
MSG_BAD_START = "`description` strings should not start with \"A...\" or \"The...\"."\
" #{MSG_STYLE_GUIDE_LINK}".freeze
MSG_CONTAINS_THIS = "`description` strings should not contain the demonstrative \"this\"."\
" #{MSG_STYLE_GUIDE_LINK}".freeze
MSG_BAD_START = "`description` strings should not start with \"A...\" or \"The...\". "\
"#{MSG_STYLE_GUIDE_LINK}".freeze
MSG_CONTAINS_THIS = "`description` strings should not contain the demonstrative \"this\". "\
"#{MSG_STYLE_GUIDE_LINK}".freeze
def_node_matcher :graphql_describable?, <<~PATTERN
(send nil? {:field :argument :value} ...)

View File

@ -9,8 +9,8 @@ module RuboCop
class AddColumnsToWideTables < RuboCop::Cop::Base
include MigrationHelpers
MSG = '`%s` is a wide table with several columns, adding more should be avoided unless absolutely necessary.' \
' Consider storing the column in a different table or creating a new one.'
MSG = '`%s` is a wide table with several columns, adding more should be avoided unless absolutely necessary. ' \
'Consider storing the column in a different table or creating a new one.'
DENYLISTED_METHODS = %i[
add_column

View File

@ -8,8 +8,8 @@ module RuboCop
class BackgroundMigrations < RuboCop::Cop::Base
include MigrationHelpers
MSG = 'Background migrations are deprecated. Please use a Batched Background Migration instead.'\
' More info: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html'
MSG = 'Background migrations are deprecated. Please use a Batched Background Migration instead. '\
'More info: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html'
def on_send(node)
name = node.children[1]

View File

@ -113,5 +113,4 @@ UsageData/InstrumentationSuperclass:
- :RedisHLLMetric
- :RedisMetric
- :NumbersMetric
- :AggregatedMetric
- :PrometheusMetric

View File

@ -108,8 +108,8 @@ module Glfm
# headers should be size 3 or less [<H1_headertext>, <H2_headertext>, <H3_headertext>]
if headers.length == 1 && line =~ h3_regex
errmsg = "Error: The H3 '#{headertext}' may not be nested directly within the H1 '#{headers[0]}'. " \
" Add an H2 header before the H3 header."
errmsg = "Error: The H3 '#{headertext}' may not be nested directly within the H1 '#{headers[0]}'. " \
"Add an H2 header before the H3 header."
raise errmsg
end

View File

@ -104,7 +104,7 @@ class PipelineTestReportBuilder
fetch("#{pipeline_url}/tests/suite.json?build_ids[]=#{build_id}").tap do |suite|
suite['job_url'] = job_url(pipeline_url, build_id)
end
rescue Net::HTTPServerException => e
rescue Net::HTTPClientException => e
raise e unless e.response.code.to_i == 404
puts "[PipelineTestReportBuilder] Artifacts not found. They may have expired. Skipping this build."

View File

@ -74,7 +74,8 @@ else
puts missing_message % missing_testcases.join("\n") unless missing_testcases.empty?
puts format_message % testcase_format_errors.join("\n") unless testcase_format_errors.empty?
puts "\n*** Please link a unique test case from the GitLab project for the errors listed above.\n"
puts " See: https://docs.gitlab.com/ee/development/testing_guide/end_to_end/best_practices.html#link-a-test-to-its-test-case"\
" for further details on how to create test cases"
puts " See: https://docs.gitlab.com/ee/development/testing_guide/end_to_end/" \
"best_practices.html#link-a-test-to-its-test-case " \
"for further details on how to create test cases"
exit 1
end

View File

@ -57,8 +57,7 @@ module DeprecationToolkitEnv
# the dependency causing the problem.
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
def self.allowed_kwarg_warning_paths
%w[
]
%w[]
end
def self.configure!

View File

@ -33,7 +33,7 @@ RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :co
let(:feature_flags_state) { true }
before do
stub_feature_flags(merge_when_checks_pass: feature_flags_state, merge_blocked_component: feature_flags_state)
stub_feature_flags(merge_when_checks_pass: feature_flags_state)
create(:ci_build, pipeline: pipeline)
@ -73,7 +73,8 @@ RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :co
it 'retains merge request data after clicking Resolve WIP status' do
expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}")
expect(page).to have_content "Merge blocked: Select Mark as ready to remove it from Draft status."
expect(page).to have_content "Merge blocked: 1 check failed"
expect(page).to have_content "Merge request must not be draft"
page.within('.mr-state-widget') do
click_button('Mark as ready')

View File

@ -15,6 +15,7 @@ import ProjectsExploreFilteredSearchAndSort from '~/projects/explore/components/
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { visitUrl } from '~/lib/utils/url_utility';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@ -74,6 +75,20 @@ describe('ProjectsExploreFilteredSearchAndSort', () => {
{ value: '11', title: 'Shell' },
],
},
{
type: 'min_access_level',
icon: 'user',
title: 'Role',
token: GlFilteredSearchToken,
unique: true,
operators: OPERATORS_IS,
options: [
{
value: '50',
title: 'Owner',
},
],
},
],
filteredSearchQuery: { [FILTERED_SEARCH_TERM_KEY]: 'foo' },
filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY,

View File

@ -1,417 +0,0 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlModal } from '@gitlab/ui';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import rebaseQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
import eventHub from '~/vue_merge_request_widget/event_hub';
import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
import toast from '~/vue_shared/plugins/global_toast';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
jest.mock('~/vue_shared/plugins/global_toast');
let wrapper;
const showMock = jest.fn();
const mockPipelineNodes = [
{
id: '1',
project: {
id: '2',
fullPath: 'user/forked',
},
},
];
const mockQueryHandler = ({
rebaseInProgress = false,
targetBranch = '',
pushToSourceBranch = false,
nodes = mockPipelineNodes,
} = {}) =>
jest.fn().mockResolvedValue({
data: {
project: {
id: '1',
mergeRequest: {
id: '2',
rebaseInProgress,
targetBranch,
userPermissions: {
pushToSourceBranch,
},
pipelines: {
nodes,
},
},
},
},
});
const createMockApolloProvider = (handler) => {
Vue.use(VueApollo);
return createMockApollo([[rebaseQuery, handler]]);
};
function createWrapper({ propsData = {}, provideData = {}, handler = mockQueryHandler() } = {}) {
wrapper = shallowMountExtended(WidgetRebase, {
apolloProvider: createMockApolloProvider(handler),
provide: {
...provideData,
},
propsData: {
mr: {},
service: {},
...propsData,
},
stubs: {
StateContainer,
GlModal: stubComponent(GlModal, {
methods: {
show: showMock,
},
}),
},
});
}
describe('Merge request widget rebase component', () => {
const findRebaseMessage = () => wrapper.findByTestId('rebase-message');
const findBoldText = () => wrapper.findComponent(BoldText);
const findRebaseMessageText = () => findRebaseMessage().text();
const findStandardRebaseButton = () => wrapper.findByTestId('standard-rebase-button');
const findRebaseWithoutCiButton = () => wrapper.findByTestId('rebase-without-ci-button');
const findModal = () => wrapper.findComponent(GlModal);
describe('while rebasing', () => {
it('should show progress message', async () => {
createWrapper({
handler: mockQueryHandler({ rebaseInProgress: true }),
});
await waitForPromises();
expect(findRebaseMessageText()).toContain('Rebase in progress');
});
});
describe('with permissions', () => {
const rebaseMock = jest.fn().mockResolvedValue();
const pollMock = jest.fn().mockResolvedValue({});
it('renders the warning message', async () => {
createWrapper({
handler: mockQueryHandler({
rebaseInProgress: false,
pushToSourceBranch: false,
}),
});
await waitForPromises();
expect(findBoldText().props('message')).toContain('Merge blocked');
expect(findBoldText().props('message').replace(/\s\s+/g, ' ')).toContain(
'the source branch must be rebased onto the target branch',
);
});
it('renders an error message when rebasing has failed', async () => {
createWrapper({
propsData: {
service: {
rebase: jest.fn().mockRejectedValue({
response: {
data: {
merge_error: 'Something went wrong!',
},
},
}),
},
},
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
findStandardRebaseButton().vm.$emit('click');
await waitForPromises();
expect(findRebaseMessageText()).toContain('Something went wrong!');
});
describe('Rebase buttons', () => {
it('renders both buttons', async () => {
createWrapper({
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
expect(findRebaseWithoutCiButton().exists()).toBe(true);
expect(findStandardRebaseButton().exists()).toBe(true);
});
it('starts the rebase when clicking', async () => {
createWrapper({
propsData: {
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
findStandardRebaseButton().vm.$emit('click');
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
});
it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
createWrapper({
propsData: {
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
findRebaseWithoutCiButton().vm.$emit('click');
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
describe('Rebase when pipelines must succeed is enabled', () => {
beforeEach(async () => {
createWrapper({
propsData: {
mr: {
onlyAllowMergeIfPipelineSucceeds: true,
},
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
});
it('renders only the rebase button', () => {
expect(findRebaseWithoutCiButton().exists()).toBe(false);
expect(findStandardRebaseButton().exists()).toBe(true);
});
it('starts the rebase when clicking', async () => {
findStandardRebaseButton().vm.$emit('click');
await nextTick();
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
});
});
describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
beforeEach(async () => {
createWrapper({
propsData: {
mr: {
onlyAllowMergeIfPipelineSucceeds: true,
allowMergeOnSkippedPipeline: true,
},
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
});
it('renders both rebase buttons', () => {
expect(findRebaseWithoutCiButton().exists()).toBe(true);
expect(findStandardRebaseButton().exists()).toBe(true);
});
it('starts the rebase when clicking', async () => {
findStandardRebaseButton().vm.$emit('click');
await nextTick();
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
});
it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
findRebaseWithoutCiButton().vm.$emit('click');
await nextTick();
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
describe('security modal', () => {
it('displays modal and rebases after confirming', async () => {
createWrapper({
propsData: {
mr: {
sourceProjectFullPath: 'user/forked',
targetProjectFullPath: 'root/original',
},
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
provideData: { canCreatePipelineInTargetProject: true },
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
findStandardRebaseButton().vm.$emit('click');
expect(showMock).toHaveBeenCalled();
findModal().vm.$emit('primary');
expect(rebaseMock).toHaveBeenCalled();
});
it('does not display modal', async () => {
createWrapper({
propsData: {
mr: {
sourceProjectFullPath: 'user/forked',
targetProjectFullPath: 'root/original',
},
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
provideData: { canCreatePipelineInTargetProject: false },
handler: mockQueryHandler({ pushToSourceBranch: true }),
});
await waitForPromises();
findStandardRebaseButton().vm.$emit('click');
expect(showMock).not.toHaveBeenCalled();
expect(rebaseMock).toHaveBeenCalled();
});
});
});
describe('without permissions', () => {
const exampleTargetBranch = 'fake-branch-to-test-with';
describe('UI text', () => {
beforeEach(async () => {
createWrapper({
handler: mockQueryHandler({
pushToSourceBranch: false,
targetBranch: exampleTargetBranch,
}),
});
await waitForPromises();
});
it('renders a message explaining user does not have permissions', () => {
expect(findBoldText().props('message')).toContain('Merge blocked');
expect(findBoldText().props('message')).toContain('the source branch must be rebased');
});
it('renders the correct target branch name', () => {
expect(findBoldText().props('message')).toContain('Merge blocked:');
expect(findBoldText().props('message')).toContain(
'the source branch must be rebased onto the target branch.',
);
});
});
it('does render the "Rebase without pipeline" button', async () => {
createWrapper({
handler: mockQueryHandler({
rebaseInProgress: false,
pushToSourceBranch: false,
targetBranch: exampleTargetBranch,
}),
});
await waitForPromises();
expect(findRebaseWithoutCiButton().exists()).toBe(true);
});
});
describe('methods', () => {
it('checkRebaseStatus', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
createWrapper({
propsData: {
service: {
rebase() {
return Promise.resolve();
},
poll() {
return Promise.resolve({
data: {
rebase_in_progress: false,
should_be_rebased: false,
merge_error: null,
},
});
},
},
},
});
await waitForPromises();
findRebaseWithoutCiButton().vm.$emit('click');
// Wait for the rebase request
await nextTick();
// Wait for the polling request
await nextTick();
// Wait for the eventHub to be called
await nextTick();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
expect(toast).toHaveBeenCalledWith('Rebase completed');
});
});
// This may happen when the session of a user is expired.
// see https://gitlab.com/gitlab-org/gitlab/-/issues/413627
describe('with empty project', () => {
it('does not throw any error', async () => {
const fn = async () => {
createWrapper({
handler: jest.fn().mockResolvedValue({ data: { project: null } }),
});
await waitForPromises();
};
await expect(fn()).resolves.not.toThrow();
});
});
});

View File

@ -1,26 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import MergeChecksFailed from '~/vue_merge_request_widget/components/states/merge_checks_failed.vue';
import { DETAILED_MERGE_STATUS } from '~/vue_merge_request_widget/constants';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
let wrapper;
function factory(propsData = {}) {
wrapper = shallowMount(MergeChecksFailed, {
propsData,
});
}
describe('Merge request widget merge checks failed state component', () => {
it.each`
mrState | displayText
${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
${{ detailedMergeStatus: DETAILED_MERGE_STATUS.BLOCKED_STATUS }} | ${'blockingMergeRequests'}
${{ detailedMergeStatus: DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS }} | ${'externalStatusChecksFailed'}
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
const message = wrapper.findComponent(BoldText).props('message');
expect(message).toContain(MergeChecksFailed.i18n[displayText]);
});
});

View File

@ -1,315 +0,0 @@
import { mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { removeBreakLine } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql';
import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
Vue.use(VueApollo);
describe('MRWidgetConflicts', () => {
let wrapper;
const path = '/conflicts';
const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button');
const findMergeLocalButton = () => wrapper.findByTestId('merge-locally-button');
const mergeConflictsText = 'merge conflicts must be resolved.';
const fastForwardMergeText =
'fast-forward merge is not possible. To merge this request, first rebase locally.';
const userCannotMergeText =
'Users who can write to the source or target branches can resolve the conflicts.';
const resolveConflictsBtnText = 'Resolve conflicts';
const mergeLocallyBtnText = 'Resolve locally';
const defaultApolloProvider = (mockData = {}) => {
const userData = {
data: {
project: {
id: 234,
mergeRequest: {
id: 234,
userPermissions: {
canMerge: mockData.canMerge || false,
pushToSourceBranch: mockData.canPushToSourceBranch || false,
},
},
},
},
};
const mrData = {
data: {
project: {
id: 234,
mergeRequest: {
id: 234,
shouldBeRebased: mockData.shouldBeRebased || false,
sourceBranchProtected: mockData.sourceBranchProtected || false,
userPermissions: {
pushToSourceBranch: mockData.canPushToSourceBranch || false,
},
},
},
},
};
return createMockApollo([
[userPermissionsQuery, jest.fn().mockResolvedValue(userData)],
[conflictsStateQuery, jest.fn().mockResolvedValue(mrData)],
]);
};
async function createComponent({
propsData,
queryData,
apolloProvider = defaultApolloProvider(queryData),
} = {}) {
wrapper = extendedWrapper(
mount(ConflictsComponent, {
apolloProvider,
propsData,
}),
);
await waitForPromises();
}
// There are two permissions we need to consider:
//
// 1. Is the user allowed to merge to the target branch?
// 2. Is the user allowed to push to the source branch?
//
// This yields 4 possible permutations that we need to test, and
// we test them below. A user who can push to the source
// branch should be allowed to resolve conflicts. This is
// consistent with what the backend does.
describe('when allowed to merge but not allowed to push to source branch', () => {
beforeEach(async () => {
await createComponent({
propsData: {
mr: {
conflictsDocsPath: '',
},
},
queryData: {
canMerge: true,
canPushToSourceBranch: false,
conflictResolutionPath: path,
},
});
});
it('should tell you about conflicts without bothering other people', () => {
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain(mergeConflictsText);
expect(text).not.toContain(userCannotMergeText);
});
it('should not allow you to resolve the conflicts', () => {
expect(wrapper.text()).not.toContain(resolveConflictsBtnText);
});
it('should have merge buttons', () => {
expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
});
});
describe('when not allowed to merge but allowed to push to source branch', () => {
beforeEach(async () => {
await createComponent({
propsData: {
mr: {
conflictResolutionPath: path,
conflictsDocsPath: '',
},
},
queryData: {
canMerge: false,
canPushToSourceBranch: true,
},
});
});
it('should tell you about conflicts', () => {
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain(userCannotMergeText);
});
it('should allow you to resolve the conflicts', () => {
expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
expect(findResolveButton().attributes('href')).toEqual(path);
});
it('should not have merge buttons', () => {
expect(wrapper.text()).not.toContain(mergeLocallyBtnText);
});
});
describe('when allowed to merge and push to source branch', () => {
beforeEach(async () => {
await createComponent({
queryData: {
canMerge: true,
canPushToSourceBranch: true,
},
propsData: {
mr: {
conflictResolutionPath: path,
conflictsDocsPath: '',
},
},
});
});
it('should tell you about conflicts without bothering other people', () => {
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain(mergeConflictsText);
expect(text).not.toContain(userCannotMergeText);
});
it('should allow you to resolve the conflicts', () => {
expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
expect(findResolveButton().attributes('href')).toEqual(path);
});
it('should have merge buttons', () => {
expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
});
});
describe('when user does not have permission to push to source branch', () => {
it('should show proper message', async () => {
await createComponent({
propsData: {
mr: {
conflictsDocsPath: '',
},
},
queryData: {
canMerge: false,
canPushToSourceBranch: false,
},
});
expect(wrapper.text().trim().replace(/\s\s+/g, ' ')).toContain(userCannotMergeText);
});
it('should not have action buttons', async () => {
await createComponent({
queryData: {
canMerge: false,
canPushToSourceBranch: false,
},
propsData: {
mr: {
conflictsDocsPath: '',
},
},
});
expect(findResolveButton().exists()).toBe(false);
expect(findMergeLocalButton().exists()).toBe(false);
});
it('should not have resolve button when no conflict resolution path', async () => {
await createComponent({
propsData: {
mr: {
conflictResolutionPath: null,
conflictsDocsPath: '',
},
},
queryData: {
canMerge: true,
},
});
expect(findResolveButton().exists()).toBe(false);
});
});
describe('when fast-forward or semi-linear merge enabled', () => {
it('should tell you to rebase locally', async () => {
await createComponent({
propsData: {
mr: {
conflictsDocsPath: '',
},
},
queryData: {
shouldBeRebased: true,
},
});
expect(removeBreakLine(wrapper.text()).trim()).toContain(fastForwardMergeText);
});
});
describe('when source branch protected', () => {
beforeEach(async () => {
await createComponent({
propsData: {
mr: {
conflictResolutionPath: TEST_HOST,
conflictsDocsPath: '',
},
},
queryData: {
canMerge: true,
sourceBranchProtected: true,
canPushToSourceBranch: true,
},
});
});
it('should not allow you to resolve the conflicts', () => {
expect(findResolveButton().exists()).toBe(false);
});
});
describe('when source branch not protected', () => {
beforeEach(async () => {
await createComponent({
propsData: {
mr: {
conflictResolutionPath: TEST_HOST,
conflictsDocsPath: '',
},
},
queryData: {
canPushToSourceBranch: true,
canMerge: true,
sourceBranchProtected: false,
},
});
});
it('should allow you to resolve the conflicts', () => {
expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
expect(findResolveButton().attributes('href')).toEqual(TEST_HOST);
});
});
describe('error states', () => {
it('when project is null due to expired session it does not throw', async () => {
const fn = async () => {
await createComponent({
propsData: { mr: {} },
apolloProvider: createMockApollo([
[conflictsStateQuery, jest.fn().mockResolvedValue({ data: { project: null } })],
[userPermissionsQuery, jest.fn().mockResolvedValue({ data: { project: null } })],
]),
});
await waitForPromises();
};
await expect(fn()).resolves.not.toThrow();
});
});
});

View File

@ -1,25 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetNotAllowed', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(notAllowedComponent);
});
it('renders success icon', () => {
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('success');
});
it('renders informative text', () => {
const message = wrapper.findComponent(BoldText).props('message');
expect(message).toContain('Ready to be merged automatically.');
expect(message).toContain(
'Ask someone with write access to this repository to merge this request',
);
});
});

View File

@ -1,25 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetPipelineBlocked', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(PipelineBlockedComponent);
});
it('renders error icon', () => {
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed');
});
it('renders information text', () => {
const message = wrapper.findComponent(BoldText).props('message');
expect(message).toContain('Merge blocked:');
expect(message).toContain(
"pipeline must succeed. It's waiting for a manual action to continue.",
);
});
});

View File

@ -1,48 +0,0 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { removeBreakLine } from 'helpers/text_helper';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('PipelineFailed', () => {
let wrapper;
const createComponent = (mr = {}) => {
wrapper = shallowMount(PipelineFailed, {
propsData: {
mr,
},
stubs: {
GlSprintf,
},
});
};
it('should render error status icon', () => {
createComponent();
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed');
});
it('should render error message with a disabled merge button', () => {
createComponent();
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain('Merge blocked:');
expect(text).toContain('pipeline must succeed');
expect(text).toContain('Push a commit that fixes the failure');
expect(wrapper.findComponent(GlLink).text()).toContain('learn about other solutions');
});
it('should render pipeline blocked message', () => {
createComponent({ isPipelineBlocked: true });
const message = wrapper.findComponent(BoldText).props('message');
expect(message).toContain('Merge blocked:');
expect(message).toContain(
"pipeline must succeed. It's waiting for a manual action to continue.",
);
});
});

View File

@ -1,55 +0,0 @@
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { removeBreakLine } from 'helpers/text_helper';
import notesEventHub from '~/notes/event_hub';
import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
function createComponent({ path = '' } = {}) {
return mount(UnresolvedDiscussions, {
propsData: {
mr: {
createIssueToResolveDiscussionsPath: path,
},
},
});
}
describe('UnresolvedDiscussions', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
it('triggers the correct notes event when the go to first unresolved discussion button is clicked', () => {
jest.spyOn(notesEventHub, '$emit');
wrapper.find('[data-testid="jump-to-first"]').trigger('click');
expect(notesEventHub.$emit).toHaveBeenCalledWith('jumpToFirstUnresolvedDiscussion');
});
describe('with threads path', () => {
beforeEach(() => {
wrapper = createComponent({ path: TEST_HOST });
});
it('should have correct elements', () => {
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain('Merge blocked:');
expect(text).toContain('all threads must be resolved.');
expect(wrapper.element.innerText).toContain('Go to first unresolved thread');
});
});
describe('without threads path', () => {
it('should not show create issue link if user cannot create issue', () => {
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain('Merge blocked:');
expect(text).toContain('all threads must be resolved.');
expect(wrapper.element.innerText).toContain('Go to first unresolved thread');
});
});
});

View File

@ -1,198 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import { createAlert } from '~/alert';
import WorkInProgress, {
MSG_SOMETHING_WENT_WRONG,
MSG_MARK_READY,
} from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import draftQuery from '~/vue_merge_request_widget/queries/states/draft.query.graphql';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
import removeDraftMutation from '~/vue_merge_request_widget/queries/toggle_draft.mutation.graphql';
import MergeRequest from '~/merge_request';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
Vue.use(VueApollo);
const TEST_PROJECT_ID = getStateQueryResponse.data.project.id;
const TEST_MR_ID = getStateQueryResponse.data.project.mergeRequest.id;
const TEST_MR_IID = '23';
const TEST_MR_TITLE = 'Test MR Title';
const TEST_PROJECT_PATH = 'lorem/ipsum';
jest.mock('~/alert');
jest.mock('~/merge_request', () => ({ toggleDraftStatus: jest.fn() }));
describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () => {
let wrapper;
let apolloProvider;
let draftQuerySpy;
let removeDraftMutationSpy;
const findWIPButton = () => wrapper.findByTestId('removeWipButton');
const createDraftQueryResponse = (canUpdateMergeRequest) => ({
data: {
project: {
__typename: 'Project',
id: TEST_PROJECT_ID,
mergeRequest: {
__typename: 'MergeRequest',
id: TEST_MR_ID,
draft: true,
title: TEST_MR_TITLE,
mergeableDiscussionsState: false,
userPermissions: {
updateMergeRequest: canUpdateMergeRequest,
},
},
},
},
});
const createRemoveDraftMutationResponse = () => ({
data: {
mergeRequestSetDraft: {
__typename: 'MergeRequestSetWipPayload',
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
id: TEST_MR_ID,
title: TEST_MR_TITLE,
draft: false,
mergeableDiscussionsState: true,
},
},
},
});
const createComponent = async () => {
wrapper = mountExtended(WorkInProgress, {
apolloProvider,
propsData: {
mr: {
issuableId: TEST_MR_ID,
title: TEST_MR_TITLE,
iid: TEST_MR_IID,
targetProjectFullPath: TEST_PROJECT_PATH,
},
},
});
await waitForPromises();
// why: work_in_progress.vue has some coupling that this query has been read before
// for some reason this has to happen **after** the component has mounted
// or apollo throws errors.
apolloProvider.defaultClient.cache.writeQuery({
query: getStateQuery,
variables: {
projectPath: TEST_PROJECT_PATH,
iid: TEST_MR_IID,
},
data: getStateQueryResponse.data,
});
};
beforeEach(() => {
draftQuerySpy = jest.fn().mockResolvedValue(createDraftQueryResponse(true));
removeDraftMutationSpy = jest.fn().mockResolvedValue(createRemoveDraftMutationResponse());
apolloProvider = createMockApollo([
[draftQuery, draftQuerySpy],
[removeDraftMutation, removeDraftMutationSpy],
]);
});
describe('when user can update MR', () => {
beforeEach(async () => {
await createComponent();
});
it('renders text', () => {
const message = wrapper.text();
expect(message).toContain('Merge blocked:');
expect(message).toContain('Select Mark as ready to remove it from Draft status.');
});
it('renders mark ready button', () => {
expect(findWIPButton().text()).toBe(MSG_MARK_READY);
});
it('does not call remove draft mutation', () => {
expect(removeDraftMutationSpy).not.toHaveBeenCalled();
});
describe('when mark ready button is clicked', () => {
beforeEach(async () => {
findWIPButton().vm.$emit('click');
await waitForPromises();
});
it('calls mutation spy', () => {
expect(removeDraftMutationSpy).toHaveBeenCalledWith({
draft: false,
iid: TEST_MR_IID,
projectPath: TEST_PROJECT_PATH,
});
});
it('does not create alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
it('calls toggleDraftStatus', () => {
expect(MergeRequest.toggleDraftStatus).toHaveBeenCalledWith(TEST_MR_TITLE, true);
});
});
describe('when mutation fails and ready button is clicked', () => {
beforeEach(async () => {
removeDraftMutationSpy.mockRejectedValue(new Error('TEST FAIL'));
findWIPButton().vm.$emit('click');
await waitForPromises();
});
it('creates alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: MSG_SOMETHING_WENT_WRONG,
});
});
it('does not call toggleDraftStatus', () => {
expect(MergeRequest.toggleDraftStatus).not.toHaveBeenCalled();
});
});
});
describe('when user cannot update MR', () => {
beforeEach(async () => {
draftQuerySpy.mockResolvedValue(createDraftQueryResponse(false));
createComponent();
await waitForPromises();
});
it('does not render mark ready button', () => {
expect(findWIPButton().exists()).toBe(false);
});
});
describe('when project is null', () => {
beforeEach(async () => {
draftQuerySpy.mockResolvedValue({ data: { project: null } });
createComponent();
await waitForPromises();
});
// This is to mitigate https://gitlab.com/gitlab-org/gitlab/-/issues/413627
it('does not throw any error', () => {
expect(wrapper.exists()).toBe(true);
});
});
});

View File

@ -19,7 +19,6 @@ import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/consta
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ConflictsState from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
import MergedState from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
@ -35,6 +34,8 @@ import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/appro
import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql';
import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
import mergeChecksQuery from '~/vue_merge_request_widget/queries/merge_checks.query.graphql';
import mergeChecksSubscription from '~/vue_merge_request_widget/queries/merge_checks.subscription.graphql';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
@ -104,12 +105,24 @@ describe('MrWidgetOptions', () => {
jest.fn().mockResolvedValue({ data: { project: { mergeRequest: {} } } }),
],
[securityReportMergeRequestDownloadPathsQuery, jest.fn().mockResolvedValue(null)],
[
mergeChecksQuery,
jest.fn().mockResolvedValue({
data: {
project: {
id: 1,
mergeRequest: { id: 1, userPermissions: { canMerge: true }, mergeabilityChecks: [] },
},
},
}),
],
...(options.apolloMock || []),
];
const subscriptionHandlers = [
[approvedBySubscription, () => mockedApprovalsSubscription],
[getStateSubscription, stateSubscriptionHandler],
[readyToMergeSubscription, () => createMockApolloSubscription()],
[mergeChecksSubscription, () => createMockApolloSubscription()],
];
const apolloProvider = createMockApollo(queryHandlers);
@ -158,10 +171,9 @@ describe('MrWidgetOptions', () => {
describe('computed', () => {
describe('componentName', () => {
it.each`
state | componentName | component
${STATUS_MERGED} | ${'MergedState'} | ${MergedState}
${'conflicts'} | ${'ConflictsState'} | ${ConflictsState}
${'shaMismatch'} | ${'ShaMismatch'} | ${ShaMismatch}
state | componentName | component
${STATUS_MERGED} | ${'MergedState'} | ${MergedState}
${'shaMismatch'} | ${'ShaMismatch'} | ${ShaMismatch}
`('should translate $state into $componentName component', async ({ state, component }) => {
await createComponent();
Vue.set(wrapper.vm.mr, 'state', state);

View File

@ -22,45 +22,15 @@ describe('getStateKey', () => {
expect(bound()).toEqual('preparing');
context.detailedMergeStatus = null;
expect(bound()).toEqual('checking');
context.detailedMergeStatus = 'MERGEABLE';
expect(bound()).toEqual('readyToMerge');
context.autoMergeEnabled = true;
context.hasMergeableDiscussionsState = true;
expect(bound()).toEqual('autoMergeEnabled');
context.canMerge = true;
context.isSHAMismatch = true;
expect(bound()).toEqual('shaMismatch');
context.canMerge = false;
context.detailedMergeStatus = 'DISCUSSIONS_NOT_RESOLVED';
expect(bound()).toEqual('unresolvedDiscussions');
context.detailedMergeStatus = 'DRAFT_STATUS';
expect(bound()).toEqual('draft');
context.detailedMergeStatus = 'CI_MUST_PASS';
expect(bound()).toEqual('pipelineFailed');
context.shouldBeRebased = true;
expect(bound()).toEqual('rebase');
context.hasConflicts = true;
expect(bound()).toEqual('conflicts');
context.detailedMergeStatus = 'CHECKING';
expect(bound()).toEqual('checking');
@ -78,26 +48,4 @@ describe('getStateKey', () => {
expect(bound()).toEqual('archived');
});
it('returns rebased state key', () => {
const context = {
mergeStatus: 'checked',
autoMergeEnabled: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: true,
isPipelineFailed: true,
hasMergeableDiscussionsState: false,
isPipelineBlocked: false,
canBeMerged: false,
shouldBeRebased: true,
projectArchived: false,
branchMissing: false,
commitsCount: 2,
hasConflicts: false,
draft: false,
};
const bound = getStateKey.bind(context);
expect(bound()).toEqual('rebase');
});
});

View File

@ -98,26 +98,6 @@ RSpec.describe Gitlab::Database::Partitioning::CiSlidingListStrategy, feature_ca
end
end
describe '#partition_for_id' do
subject(:partition_for_id) { strategy.partition_for_id(id) }
context 'when partition_id matches any partition' do
let(:id) { 101 }
it 'returns the partition' do
expect(partition_for_id).to eq(strategy.active_partition)
end
end
context 'when partition_id does not match any partition' do
let(:id) { non_existing_record_id }
it 'returns nil' do
expect(partition_for_id).to be_nil
end
end
end
describe '#missing_partitions' do
context 'when next_partition_if returns true' do
let(:next_partition_if) { proc { |partition| partition.values.max < 102 } }

View File

@ -31,6 +31,64 @@ RSpec.describe Gitlab::Database::PostgresPartition, type: :model, feature_catego
it_behaves_like 'a postgres model'
describe 'scopes' do
describe '.with_parent_tables' do
subject(:with_parent_tables) { described_class.with_parent_tables(parent_tables) }
let(:parent_tables) { ['_test_partitioned_table'] }
it 'returns all partitions with parent tables', :aggregate_failures do
results = with_parent_tables
expect(results.size).to eq(1)
expect(results.first.identifier).to eq(identifier)
end
end
describe '.with_list_constraint' do
subject(:with_list_constraint) { described_class.with_list_constraint(partition_id) }
context 'when condition matches' do
let(:partition_id) { '102' }
let(:expected_size) { Ci::Partitionable.registered_models.size }
it 'returns the partitions containing the match' do
results = with_list_constraint
expect(results.size).to eq(expected_size)
end
end
context 'when condition does not match' do
let(:partition_id) { non_existing_record_id }
it 'returns an empty relation' do
expect(with_list_constraint).to be_empty
end
end
end
describe '.above_threshold' do
subject(:above_threshold) { described_class.above_threshold(threshold) }
context 'when the partition size is above a given threshold' do
let(:threshold) { 1.byte }
it 'returns all partitions above the threshold' do
expect(above_threshold.size).not_to be_zero
end
end
context 'when the partition size is below a given threshold' do
let(:threshold) { 100.megabytes }
it 'returns an empty relation' do
expect(above_threshold).to be_empty
end
end
end
end
describe '.for_parent_table' do
let(:second_name) { '_test_partition_02' }

View File

@ -57,36 +57,6 @@ RSpec.describe Gitlab::RackAttack::Request, feature_category: :rate_limiting do
it { is_expected.to eq(expected) }
end
end
context 'when rate_limit_oauth_api feature flag is disabled' do
before do
stub_feature_flags(rate_limit_oauth_api: false)
end
where(:path, :expected) do
'/' | false
'/groups' | false
'/foo/api' | false
'/api' | true
'/api/v4/groups/1' | true
'/oauth/tokens' | false
'/oauth/userinfo' | false
end
with_them do
it { is_expected.to eq(expected) }
context 'when the application is mounted at a relative URL' do
before do
stub_config_setting(relative_url_root: '/gitlab/root')
end
it { is_expected.to eq(expected) }
end
end
end
end
describe '#api_internal_request?' do

View File

@ -318,18 +318,6 @@ RSpec.describe Gitlab::Usage::MetricDefinition, feature_category: :service_ping
'RedisHLLMetric' | { events: [2] } | false
'RedisHLLMetric' | { events: 'a' } | false
'RedisHLLMetric' | { event: ['a'] } | false
'AggregatedMetric' | { aggregate: { attribute: 'user.id' }, events: ['a'] } | true
'AggregatedMetric' | { aggregate: { attribute: 'project.id' }, events: %w[b c] } | true
'AggregatedMetric' | nil | false
'AggregatedMetric' | {} | false
'AggregatedMetric' | { aggregate: { attribute: 'user.id' }, events: ['a'], event: 'a' } | false
'AggregatedMetric' | { aggregate: { attribute: 'user.id' } } | false
'AggregatedMetric' | { events: ['a'] } | false
'AggregatedMetric' | { aggregate: { attribute: 'user.id' }, events: 'a' } | false
'AggregatedMetric' | { aggregate: 'a', events: ['a'] } | false
'AggregatedMetric' | { aggregate: {}, events: ['a'] } | false
'AggregatedMetric' | { aggregate: { attribute: 'user.id', a: 'b' }, events: ['a'] } | false
'AggregatedMetric' | { aggregate: { attribute: ['user.id'] }, events: ['a'] } | false
end
with_them do

Some files were not shown because too many files have changed in this diff Show More