Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Experiment Rollout.md11
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue3
-rw-r--r--app/assets/javascripts/experimentation/utils.js7
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue55
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue54
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql (renamed from app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js6
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss2
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_wip.rb35
-rw-r--r--app/graphql/types/merge_request_type.rb3
-rw-r--r--app/graphql/types/mutation_type.rb3
-rw-r--r--app/models/concerns/ttl_expirable.rb7
-rw-r--r--app/models/gpg_signature.rb3
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/namespaces/project_namespace.rb2
-rw-r--r--app/models/packages/npm.rb4
-rw-r--r--app/models/packages/npm/metadatum.rb25
-rw-r--r--app/models/packages/package.rb2
-rw-r--r--app/models/project.rb33
-rw-r--r--app/models/repository.rb7
-rw-r--r--app/presenters/packages/npm/package_presenter.rb38
-rw-r--r--app/services/dependency_proxy/find_or_create_blob_service.rb3
-rw-r--r--app/services/dependency_proxy/find_or_create_manifest_service.rb2
-rw-r--r--app/services/packages/npm/create_package_service.rb4
-rw-r--r--app/services/projects/destroy_service.rb14
-rw-r--r--app/validators/json_schemas/npm_package_json.json26
-rw-r--r--app/views/import/github/new.html.haml2
-rw-r--r--app/views/jira_connect/subscriptions/index.html.haml15
-rw-r--r--app/views/layouts/_published_experiments.html.haml4
-rw-r--r--app/views/layouts/application.html.haml1
-rw-r--r--app/views/projects/commit/_multiple_signatures_signature_badge.html.haml6
-rw-r--r--app/views/projects/issues/_discussion.html.haml1
-rw-r--r--app/workers/all_queues.yml18
-rw-r--r--app/workers/clusters/integrations/check_prometheus_health_worker.rb1
-rw-r--r--app/workers/dependency_proxy/image_ttl_group_policy_worker.rb5
-rw-r--r--config/feature_flags/development/create_project_namespace_on_project_create.yml8
-rw-r--r--config/feature_flags/development/multiple_gpg_signatures.yml (renamed from config/feature_flags/development/rate_limiter_safe_increment.yml)8
-rw-r--r--config/feature_flags/development/packages_npm_abbreviated_metadata.yml (renamed from config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml)10
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/postgres_partitioning.rb6
-rw-r--r--data/deprecations/templates/example.yml2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb11
-rw-r--r--db/migrate/20211028132247_create_packages_npm_metadata.rb22
-rw-r--r--db/migrate/20211105125756_add_read_at_to_dependency_proxy_manifests.rb7
-rw-r--r--db/migrate/20211105125813_add_read_at_to_dependency_proxy_blobs.rb7
-rw-r--r--db/migrate/20211108203248_update_dependency_proxy_indexes_with_read_at.rb27
-rw-r--r--db/schema_migrations/202110281322471
-rw-r--r--db/schema_migrations/202111051257561
-rw-r--r--db/schema_migrations/202111051258131
-rw-r--r--db/schema_migrations/202111082032481
-rw-r--r--db/structure.sql20
-rw-r--r--doc/api/graphql/reference/index.md26
-rw-r--r--doc/ci/yaml/index.md109
-rw-r--r--doc/development/experiment_guide/gitlab_experiment.md52
-rw-r--r--doc/development/migration_style_guide.md4
-rw-r--r--doc/user/packages/npm_registry/index.md50
-rw-r--r--doc/user/project/integrations/custom_issue_tracker.md33
-rw-r--r--doc/user/project/integrations/img/custom_issue_tracker_v14_5.pngbin0 -> 6636 bytes
-rw-r--r--doc/user/project/service_desk.md4
-rw-r--r--lib/api/concerns/packages/npm_endpoints.rb4
-rw-r--r--lib/bulk_imports/ndjson_pipeline.rb9
-rw-r--r--lib/gitlab/application_rate_limiter.rb24
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--lib/gitlab/database/partitioning.rb6
-rw-r--r--lib/gitlab/git/repository.rb3
-rw-r--r--lib/gitlab/gpg/commit.rb32
-rw-r--r--locale/gitlab.pot18
-rw-r--r--qa/chemlab-library-gitlab.gemspec1
-rw-r--r--qa/lib/gitlab.rb29
-rw-r--r--qa/lib/gitlab/page/main/welcome.rb13
-rw-r--r--qa/lib/gitlab/page/main/welcome.stub.rb33
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb6
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb21
-rw-r--r--spec/controllers/registrations_controller_spec.rb5
-rw-r--r--spec/factories/namespaces/project_namespaces.rb2
-rw-r--r--spec/factories/packages/npm/metadata.rb18
-rw-r--r--spec/features/signed_commits_spec.rb15
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json12
-rw-r--r--spec/fixtures/packages/npm/payload.json3
-rw-r--r--spec/fixtures/packages/npm/payload_with_duplicated_packages.json3
-rw-r--r--spec/frontend/__helpers__/experimentation_helper.js29
-rw-r--r--spec/frontend/boards/components/new_board_button_spec.js10
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js10
-rw-r--r--spec/frontend/experimentation/utils_spec.js171
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js165
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js50
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js8
-rw-r--r--spec/graphql/mutations/merge_requests/set_wip_spec.rb55
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb2
-rw-r--r--spec/graphql/types/mutation_type_spec.rb8
-rw-r--r--spec/lib/bulk_imports/ndjson_pipeline_spec.rb3
-rw-r--r--spec/lib/bulk_imports/projects/stage_spec.rb3
-rw-r--r--spec/lib/gitlab/application_rate_limiter_spec.rb141
-rw-r--r--spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb4
-rw-r--r--spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb3
-rw-r--r--spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb3
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb69
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb11
-rw-r--r--spec/models/concerns/loaded_in_group_list_spec.rb2
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb15
-rw-r--r--spec/models/namespaces/project_namespace_spec.rb2
-rw-r--r--spec/models/packages/npm/metadatum_spec.rb50
-rw-r--r--spec/models/packages/package_spec.rb1
-rw-r--r--spec/models/project_spec.rb68
-rw-r--r--spec/policies/namespaces/project_namespace_policy_spec.rb3
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb88
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb (renamed from spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb)8
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb20
-rw-r--r--spec/requests/api/project_import_spec.rb2
-rw-r--r--spec/services/dependency_proxy/find_or_create_blob_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb2
-rw-r--r--spec/services/groups/transfer_service_spec.rb51
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb29
-rw-r--r--spec/services/projects/create_service_spec.rb21
-rw-r--r--spec/services/projects/transfer_service_spec.rb6
-rw-r--r--spec/spec_helper.rb8
-rw-r--r--spec/support/helpers/gpg_helpers.rb1
-rw-r--r--spec/support/helpers/test_env.rb2
-rw-r--r--spec/support/matchers/project_namespace_matcher.rb28
-rw-r--r--spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/namespaces/traversal_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb15
-rw-r--r--spec/views/jira_connect/subscriptions/index.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/_published_experiments.html.haml_spec.rb35
-rw-r--r--spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb4
138 files changed, 1506 insertions, 881 deletions
diff --git a/.gitlab/issue_templates/Experiment Rollout.md b/.gitlab/issue_templates/Experiment Rollout.md
index c5fdc739943..23d7f325b14 100644
--- a/.gitlab/issue_templates/Experiment Rollout.md
+++ b/.gitlab/issue_templates/Experiment Rollout.md
@@ -105,5 +105,16 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate.
/chatops run feature set <experiment-key> false
```
+## Experiment Successful Cleanup Concerns
+
+_Items to be considered if candidate experience is to become a permanent part of GitLab_
+
+<!--
+Add a list of items raised during MR review or otherwise that may need further thought/condideration
+before making it a permanent part of the product.
+
+Example: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70451#note_727246104
+-->
+
/label ~"feature flag" ~"devops::growth" ~"growth experiment" ~"experiment-rollout" ~Engineering ~"workflow::scheduling" ~"experiment::pending"
/milestone %"Next 1-3 releases"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 46726a8fa07..238f07ac22c 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -14,7 +14,6 @@ import {
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
-import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
@@ -181,7 +180,7 @@ export default {
return this.diffFile.renamed_file;
},
isModeChanged() {
- return this.diffFile.viewer.name === diffViewerModes.mode_changed;
+ return this.diffFile.mode_changed;
},
expandDiffToFullFileTitle() {
if (this.diffFile.isShowingFullFile) {
diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js
index 1567bf14d0f..9ae6f6b3fdd 100644
--- a/app/assets/javascripts/experimentation/utils.js
+++ b/app/assets/javascripts/experimentation/utils.js
@@ -3,7 +3,12 @@ import { get } from 'lodash';
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
function getExperimentsData() {
- return get(window, ['gon', 'experiment'], {});
+ // Pull from deprecated window.gon.experiment
+ const experimentsFromGon = get(window, ['gon', 'experiment'], {});
+ // Pull from preferred window.gl.experiments
+ const experimentsFromGl = get(window, ['gl', 'experiments'], {});
+
+ return { ...experimentsFromGon, ...experimentsFromGl };
}
function convertExperimentDataToExperimentContext(experimentData) {
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index 9b3cd04c4e2..c0504cbb645 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -1,5 +1,6 @@
<script>
-import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import { mapState, mapMutations } from 'vuex';
import { retrieveAlert } from '~/jira_connect/subscriptions/utils';
import { SET_ALERT } from '../store/mutation_types';
@@ -13,6 +14,7 @@ export default {
GlAlert,
GlLink,
GlSprintf,
+ GlEmptyState,
SubscriptionsList,
AddNamespaceButton,
SignInButton,
@@ -21,12 +23,18 @@ export default {
usersPath: {
default: '',
},
+ subscriptions: {
+ default: [],
+ },
},
computed: {
...mapState(['alert']),
shouldShowAlert() {
return Boolean(this.alert?.message);
},
+ hasSubscriptions() {
+ return !isEmpty(this.subscriptions);
+ },
userSignedIn() {
return Boolean(!this.usersPath);
},
@@ -66,15 +74,44 @@ export default {
</template>
</gl-alert>
- <h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
-
- <div class="jira-connect-app-body gl-my-7 gl-px-5 gl-pb-4">
- <div class="gl-display-flex gl-justify-content-end">
- <sign-in-button v-if="!userSignedIn" :users-path="usersPath" />
- <add-namespace-button v-else />
- </div>
+ <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
+ <div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7">
+ <template v-if="hasSubscriptions">
+ <div class="gl-display-flex gl-justify-content-end">
+ <sign-in-button v-if="!userSignedIn" :users-path="usersPath" />
+ <add-namespace-button v-else />
+ </div>
- <subscriptions-list />
+ <subscriptions-list />
+ </template>
+ <template v-else>
+ <div v-if="!userSignedIn" class="gl-text-center">
+ <p class="gl-mb-7">{{ s__('JiraService|Sign in to GitLab.com to get started.') }}</p>
+ <sign-in-button class="gl-mb-7" :users-path="usersPath">
+ {{ __('Sign in to GitLab') }}
+ </sign-in-button>
+ <p>
+ {{
+ s__(
+ 'Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).',
+ )
+ }}
+ </p>
+ </div>
+ <gl-empty-state
+ v-else
+ :title="s__('Integrations|No linked namespaces')"
+ :description="
+ s__(
+ 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
+ )
+ "
+ >
+ <template #actions>
+ <add-namespace-button />
+ </template>
+ </gl-empty-state>
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
index 7062fb370ed..33126040c16 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { GlButton, GlTable } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapMutations } from 'vuex';
import { removeSubscription } from '~/jira_connect/subscriptions/api';
@@ -12,7 +12,6 @@ import GroupItemName from './group_item_name.vue';
export default {
components: {
GlButton,
- GlEmptyState,
GlTable,
GroupItemName,
TimeagoTooltip,
@@ -44,17 +43,15 @@ export default {
},
],
i18n: {
- emptyTitle: s__('Integrations|No linked namespaces'),
- emptyDescription: s__(
- 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
- ),
unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'),
},
methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
- isEmpty,
+ isUnlinkButtonDisabled(item) {
+ return !isEmpty(item);
+ },
isLoadingItem(item) {
return this.loadingItem === item;
},
@@ -81,29 +78,22 @@ export default {
</script>
<template>
- <div>
- <gl-empty-state
- v-if="isEmpty(subscriptions)"
- :title="$options.i18n.emptyTitle"
- :description="$options.i18n.emptyDescription"
- />
- <gl-table v-else :items="subscriptions" :fields="$options.fields">
- <template #cell(name)="{ item }">
- <group-item-name :group="item.group" />
- </template>
- <template #cell(created_at)="{ item }">
- <timeago-tooltip :time="item.created_at" />
- </template>
- <template #cell(actions)="{ item }">
- <gl-button
- :class="unlinkBtnClass(item)"
- category="secondary"
- :loading="isLoadingItem(item)"
- :disabled="!isEmpty(loadingItem)"
- @click.prevent="onClick(item)"
- >{{ __('Unlink') }}</gl-button
- >
- </template>
- </gl-table>
- </div>
+ <gl-table :items="subscriptions" :fields="$options.fields">
+ <template #cell(name)="{ item }">
+ <group-item-name :group="item.group" />
+ </template>
+ <template #cell(created_at)="{ item }">
+ <timeago-tooltip :time="item.created_at" />
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button
+ :class="unlinkBtnClass(item)"
+ category="secondary"
+ :loading="isLoadingItem(item)"
+ :disabled="isUnlinkButtonDisabled(loadingItem)"
+ @click.prevent="onClick(item)"
+ >{{ __('Unlink') }}</gl-button
+ >
+ </template>
+ </gl-table>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 790870ee4c6..fa4f8b76cb9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -10,8 +10,8 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import getStateQuery from '../../queries/get_state.query.graphql';
-import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql';
-import removeWipMutation from '../../queries/toggle_wip.mutation.graphql';
+import draftQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
@@ -23,7 +23,7 @@ export default {
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
- query: workInProgressQuery,
+ query: draftQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
@@ -53,25 +53,25 @@ export default {
},
},
methods: {
- removeWipMutation() {
+ removeDraftMutation() {
const { mergeRequestQueryVariables } = this;
this.isMakingRequest = true;
this.$apollo
.mutate({
- mutation: removeWipMutation,
+ mutation: removeDraftMutation,
variables: {
...mergeRequestQueryVariables,
- wip: false,
+ draft: false,
},
update(
store,
{
data: {
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
errors,
- mergeRequest: { mergeableDiscussionsState, workInProgress, title },
+ mergeRequest: { mergeableDiscussionsState, draft, title },
},
},
},
@@ -91,7 +91,7 @@ export default {
const data = produce(sourceData, (draftState) => {
draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
- draftState.project.mergeRequest.workInProgress = workInProgress;
+ draftState.project.mergeRequest.draft = draft;
draftState.project.mergeRequest.title = title;
});
@@ -104,14 +104,14 @@ export default {
optimisticResponse: {
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
__typename: 'MergeRequestSetWipPayload',
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
mergeableDiscussionsState: true,
title: this.mr.title,
- workInProgress: false,
+ draft: false,
},
},
},
@@ -119,7 +119,7 @@ export default {
.then(
({
data: {
- mergeRequestSetWip: {
+ mergeRequestSetDraft: {
mergeRequest: { title },
},
},
@@ -137,9 +137,9 @@ export default {
this.isMakingRequest = false;
});
},
- handleRemoveWIP() {
+ handleRemoveDraft() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
- this.removeWipMutation();
+ this.removeDraftMutation();
} else {
this.isMakingRequest = true;
this.service
@@ -178,8 +178,8 @@ export default {
size="small"
:disabled="isMakingRequest"
:loading="isMakingRequest"
- class="js-remove-wip gl-ml-3"
- @click="handleRemoveWIP"
+ class="js-remove-draft gl-ml-3"
+ @click="handleRemoveDraft"
>
{{ s__('mrWidget|Mark as ready') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index 871aa880b36..bfb1517be81 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -23,7 +23,7 @@ query getState($projectPath: ID!, $iid: String!) {
userPermissions {
canMerge
}
- workInProgress
+ draft
}
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
index 0983c28448e..0983c28448e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
new file mode 100644
index 00000000000..200fb1b7ca5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_draft.mutation.graphql
@@ -0,0 +1,10 @@
+mutation toggleDraftStatus($projectPath: ID!, $iid: String!, $draft: Boolean!) {
+ mergeRequestSetDraft(input: { projectPath: $projectPath, iid: $iid, draft: $draft }) {
+ mergeRequest {
+ mergeableDiscussionsState
+ title
+ draft
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
deleted file mode 100644
index cfaa198d516..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-mutation toggleWIPStatus($projectPath: ID!, $iid: String!, $wip: Boolean!) {
- mergeRequestSetWip(input: { projectPath: $projectPath, iid: $iid, wip: $wip }) {
- mergeRequest {
- mergeableDiscussionsState
- title
- workInProgress
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index d52371032eb..2ae4f4da2f3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -17,8 +17,8 @@ export default function deviseState() {
return stateKey.rebase;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return stateKey.pipelineFailed;
- } else if (this.workInProgress) {
- return stateKey.workInProgress;
+ } else if (this.draft) {
+ return stateKey.draft;
} else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 6628225cd46..10a2907c81a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -164,7 +164,7 @@ export default class MergeRequestStore {
this.projectArchived = data.project_archived;
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.shouldBeRebased = Boolean(data.should_be_rebased);
- this.workInProgress = data.work_in_progress;
+ this.draft = data.draft;
}
const currentUser = data.current_user;
@@ -207,7 +207,7 @@ export default class MergeRequestStore {
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
this.shouldBeRebased = mergeRequest.shouldBeRebased;
- this.workInProgress = mergeRequest.workInProgress;
+ this.draft = mergeRequest.draft;
this.mergeRequestState = mergeRequest.state;
this.setState();
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 4cb23407a74..9dfeaee905c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -4,7 +4,7 @@ export const stateToComponentMap = {
merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch',
- workInProgress: 'mr-widget-wip',
+ draft: 'mr-widget-wip',
readyToMerge: 'mr-widget-ready-to-merge',
nothingToMerge: 'mr-widget-nothing-to-merge',
notAllowedToMerge: 'mr-widget-not-allowed',
@@ -24,7 +24,7 @@ export const stateToComponentMap = {
export const statesToShowHelpWidget = [
'merging',
'conflicts',
- 'workInProgress',
+ 'draft',
'readyToMerge',
'checking',
'unresolvedDiscussions',
@@ -40,7 +40,7 @@ export const stateKey = {
nothingToMerge: 'nothingToMerge',
checking: 'checking',
conflicts: 'conflicts',
- workInProgress: 'workInProgress',
+ draft: 'draft',
pipelineFailed: 'pipelineFailed',
unresolvedDiscussions: 'unresolvedDiscussions',
pipelineBlocked: 'pipelineBlocked',
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index 4beb5edbe7b..9fe0490571e 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -42,8 +42,6 @@ $header-height: 40px;
.jira-connect-app-body {
max-width: 768px;
- margin-left: auto;
- margin-right: auto;
}
// needed for external_link
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index 8707381bb68..fc930ffebbd 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -113,8 +113,6 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
def send_manifest(manifest, from_cache:)
- # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- manifest.touch
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest
response.headers['Content-Length'] = manifest.size
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb
deleted file mode 100644
index 9b6b67d4b4f..00000000000
--- a/app/graphql/mutations/merge_requests/set_wip.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Mutations
- module MergeRequests
- class SetWip < Base
- graphql_name 'MergeRequestSetWip'
-
- argument :wip,
- GraphQL::Types::Boolean,
- required: true,
- description: <<~DESC
- Whether or not to set the merge request as a draft.
- DESC
-
- def resolve(project_path:, iid:, wip: nil)
- merge_request = authorized_find!(project_path: project_path, iid: iid)
- project = merge_request.project
-
- ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { wip_event: wip_event(merge_request, wip) })
- .execute(merge_request)
-
- {
- merge_request: merge_request,
- errors: errors_on_object(merge_request)
- }
- end
-
- private
-
- def wip_event(merge_request, wip)
- wip ? 'wip' : 'unwip'
- end
- end
- end
-end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 004ac364487..a8a96b0ccef 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -53,9 +53,6 @@ module Types
description: 'Indicates if the source branch is protected.'
field :target_branch, GraphQL::Types::String, null: false,
description: 'Target branch of the merge request.'
- field :work_in_progress, GraphQL::Types::Boolean, method: :work_in_progress?, null: false,
- deprecated: { reason: 'Use `draft`', milestone: '13.12' },
- description: 'Indicates if the merge request is a draft.'
field :draft, GraphQL::Types::Boolean, method: :draft?, null: false,
description: 'Indicates if the merge request is a draft.'
field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true,
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 7be995a40b2..5366283e55a 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -65,9 +65,6 @@ module Types
mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetSubscription
- mount_mutation Mutations::MergeRequests::SetWip,
- calls_gitaly: true,
- deprecated: { reason: 'Use mergeRequestSetDraft', milestone: '13.12' }
mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::MergeRequests::ReviewerRereview
diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb
index 00abe0a06e6..6d89521255c 100644
--- a/app/models/concerns/ttl_expirable.rb
+++ b/app/models/concerns/ttl_expirable.rb
@@ -5,10 +5,11 @@ module TtlExpirable
included do
validates :status, presence: true
+ default_value_for :read_at, Time.zone.now
enum status: { default: 0, expired: 1, processing: 2, error: 3 }
- scope :updated_before, ->(number_of_days) { where("updated_at <= ?", Time.zone.now - number_of_days.days) }
+ scope :read_before, ->(number_of_days) { where("read_at <= ?", Time.zone.now - number_of_days.days) }
scope :active, -> { where(status: :default) }
scope :lock_next_by, ->(sort) do
@@ -17,4 +18,8 @@ module TtlExpirable
.lock('FOR UPDATE SKIP LOCKED')
end
end
+
+ def read!
+ self.update(read_at: Time.zone.now)
+ end
end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 0c36e51120f..2775b520b2f 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -12,7 +12,8 @@ class GpgSignature < ApplicationRecord
same_user_different_email: 2,
other_user: 3,
unverified_key: 4,
- unknown_key: 5
+ unknown_key: 5,
+ multiple_signatures: 6
}
belongs_to :project
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index bd68311fcc8..5a74f8392ec 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -268,7 +268,6 @@ class MergeRequest < ApplicationRecord
from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id)
end
scope :merged, -> { with_state(:merged) }
- scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :open_and_closed, -> { with_states(:opened, :closed) }
scope :drafts, -> { where(draft: true) }
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 80abb733418..14a0c38dcf0 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -497,6 +497,10 @@ class Namespace < ApplicationRecord
Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
end
+ def project_namespace_creation_enabled?
+ Feature.enabled?(:create_project_namespace_on_project_create, self, default_enabled: :yaml)
+ end
+
private
def expire_child_caches
diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb
index d1806c1c088..22ec550dee2 100644
--- a/app/models/namespaces/project_namespace.rb
+++ b/app/models/namespaces/project_namespace.rb
@@ -4,8 +4,6 @@ module Namespaces
class ProjectNamespace < Namespace
has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace
- validates :project, presence: true
-
def self.sti_name
'Project'
end
diff --git a/app/models/packages/npm.rb b/app/models/packages/npm.rb
index e49199d911c..9221187d92a 100644
--- a/app/models/packages/npm.rb
+++ b/app/models/packages/npm.rb
@@ -9,5 +9,9 @@ module Packages
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end
+
+ def self.table_name_prefix
+ 'packages_npm_'
+ end
end
end
diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb
new file mode 100644
index 00000000000..7388c4bdbd2
--- /dev/null
+++ b/app/models/packages/npm/metadatum.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Packages::Npm::Metadatum < ApplicationRecord
+ belongs_to :package, -> { where(package_type: :npm) }, inverse_of: :npm_metadatum
+
+ validates :package, presence: true
+ # From https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
+ validates :package_json, json_schema: { filename: "npm_package_json" }
+ validate :ensure_npm_package_type
+ validate :ensure_package_json_size
+
+ private
+
+ def ensure_npm_package_type
+ return if package&.npm?
+
+ errors.add(:base, _('Package type must be NPM'))
+ end
+
+ def ensure_package_json_size
+ return if package_json.to_s.size < 20000
+
+ errors.add(:package_json, _('structure is too large'))
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index ff9e7ef4cf5..f25d0299524 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -39,6 +39,7 @@ class Packages::Package < ApplicationRecord
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
+ has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum'
has_many :build_infos, inverse_of: :package
has_many :pipelines, through: :build_infos, disable_joins: -> { disable_cross_joins_to_pipelines? }
has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
@@ -126,6 +127,7 @@ class Packages::Package < ApplicationRecord
.where(Packages::Composer::Metadatum.table_name => { target_sha: target })
end
scope :preload_composer, -> { preload(:composer_metadatum) }
+ scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
diff --git a/app/models/project.rb b/app/models/project.rb
index f7f8ae1f77c..36f48b91221 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -98,7 +98,7 @@ class Project < ApplicationRecord
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token
- before_save :ensure_project_namespace_in_sync
+ before_validation :ensure_project_namespace_in_sync
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
@@ -147,7 +147,7 @@ class Project < ApplicationRecord
belongs_to :namespace
# Sync deletion via DB Trigger to ensure we do not have
# a project without a project_namespace (or vice-versa)
- belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
+ belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id'
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
@@ -476,6 +476,7 @@ class Project < ApplicationRecord
validates :project_feature, presence: true
validates :namespace, presence: true
+ validates :project_namespace, presence: true, if: -> { self.namespace && self.root_namespace.project_namespace_creation_enabled? }
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
@@ -2919,12 +2920,28 @@ class Project < ApplicationRecord
end
def ensure_project_namespace_in_sync
- if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present?
- project_namespace.name = name
- project_namespace.path = path
- project_namespace.parent = namespace
- project_namespace.visibility_level = visibility_level
- end
+ # create project_namespace when project is created if create_project_namespace_on_project_create FF is enabled
+ build_project_namespace if project_namespace_creation_enabled?
+
+ # regardless of create_project_namespace_on_project_create FF we need
+ # to keep project and project namespace in sync if there is one
+ sync_attributes(project_namespace) if sync_project_namespace?
+ end
+
+ def project_namespace_creation_enabled?
+ new_record? && !project_namespace && self.namespace && self.root_namespace.project_namespace_creation_enabled?
+ end
+
+ def sync_project_namespace?
+ (changes.keys & %w(name path namespace_id namespace visibility_level shared_runners_enabled)).any? && project_namespace.present?
+ end
+
+ def sync_attributes(project_namespace)
+ project_namespace.name = name
+ project_namespace.path = path
+ project_namespace.parent = namespace
+ project_namespace.shared_runners_enabled = shared_runners_enabled
+ project_namespace.visibility_level = visibility_level
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 7b24ae35125..c7054b89255 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1091,6 +1091,13 @@ class Repository
after_create
true
+ rescue Gitlab::Git::Repository::RepositoryExists
+ # We do not want to call `#after_create` given that we didn't create the
+ # repo, but we obviously have a mismatch between what's in our exists cache
+ # and actual on-disk state as seen by Gitaly. Let's thus expire our caches.
+ expire_status_cache
+
+ nil
end
def create_from_bundle(bundle_path)
diff --git a/app/presenters/packages/npm/package_presenter.rb b/app/presenters/packages/npm/package_presenter.rb
index b9595eb6647..9e3308c2573 100644
--- a/app/presenters/packages/npm/package_presenter.rb
+++ b/app/presenters/packages/npm/package_presenter.rb
@@ -5,26 +5,37 @@ module Packages
class PackagePresenter
include API::Helpers::RelatedResourcesHelpers
+ # Allowed fields are those defined in the abbreviated form
+ # defined here: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
+ # except: name, version, dist, dependencies and xDependencies. Those are generated by this presenter.
+ PACKAGE_JSON_ALLOWED_FIELDS = %w[deprecated bin directories dist engines _hasShrinkwrap].freeze
+
attr_reader :name, :packages
- def initialize(name, packages)
+ def initialize(name, packages, include_metadata: false)
@name = name
@packages = packages
+ @include_metadata = include_metadata
end
def versions
package_versions = {}
packages.each_batch do |relation|
- relation.including_dependency_links
- .preload_files
- .each do |package|
- package_file = package.package_files.last
+ batched_packages = relation.including_dependency_links
+ .preload_files
+
+ if @include_metadata
+ batched_packages = batched_packages.preload_npm_metadatum
+ end
+
+ batched_packages.each do |package|
+ package_file = package.package_files.last
- next unless package_file
+ next unless package_file
- package_versions[package.version] = build_package_version(package, package_file)
- end
+ package_versions[package.version] = build_package_version(package, package_file)
+ end
end
package_versions
@@ -41,14 +52,14 @@ module Packages
end
def build_package_version(package, package_file)
- {
+ abbreviated_package_json(package).merge(
name: package.name,
version: package.version,
dist: {
shasum: package_file.file_sha1,
tarball: tarball_url(package, package_file)
}
- }.tap do |package_version|
+ ).tap do |package_version|
package_version.merge!(build_package_dependencies(package))
end
end
@@ -79,6 +90,13 @@ module Packages
Packages::Tag.for_packages(packages)
.preload_package
end
+
+ def abbreviated_package_json(package)
+ return {} unless @include_metadata
+
+ json = package.npm_metadatum&.package_json || {}
+ json.slice(*PACKAGE_JSON_ALLOWED_FIELDS)
+ end
end
end
end
diff --git a/app/services/dependency_proxy/find_or_create_blob_service.rb b/app/services/dependency_proxy/find_or_create_blob_service.rb
index 0a6db6e3d34..1b43263a3ba 100644
--- a/app/services/dependency_proxy/find_or_create_blob_service.rb
+++ b/app/services/dependency_proxy/find_or_create_blob_service.rb
@@ -30,8 +30,7 @@ module DependencyProxy
blob.save!
end
- # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- blob.touch if from_cache
+ blob.read! if from_cache
success(blob: blob, from_cache: from_cache)
end
diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb
index 055980c431f..aeb62be9f3a 100644
--- a/app/services/dependency_proxy/find_or_create_manifest_service.rb
+++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb
@@ -58,6 +58,8 @@ module DependencyProxy
def respond(from_cache: true)
if @manifest
+ @manifest.read!
+
success(manifest: @manifest, from_cache: from_cache)
else
error('Failed to download the manifest from the external registry', 503)
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index 1d5d9c38432..959e105694f 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -21,6 +21,10 @@ module Packages
::Packages::CreateDependencyService.new(package, package_dependencies).execute
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
+ if Feature.enabled?(:packages_npm_abbreviated_metadata, project)
+ package.create_npm_metadatum!(package_json: version_data)
+ end
+
package
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index ea20510f36e..b7ed9202b01 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -152,14 +152,12 @@ module Projects
deleted_count = project.commit_statuses.delete_all
- if deleted_count > 0
- Gitlab::AppLogger.info(
- class: 'Projects::DestroyService',
- project_id: project.id,
- message: 'leftover commit statuses',
- orphaned_commit_status_count: deleted_count
- )
- end
+ Gitlab::AppLogger.info(
+ class: 'Projects::DestroyService',
+ project_id: project.id,
+ message: 'leftover commit statuses',
+ orphaned_commit_status_count: deleted_count
+ )
end
# The project can have multiple webhooks with hundreds of thousands of web_hook_logs.
diff --git a/app/validators/json_schemas/npm_package_json.json b/app/validators/json_schemas/npm_package_json.json
new file mode 100644
index 00000000000..01bd874d214
--- /dev/null
+++ b/app/validators/json_schemas/npm_package_json.json
@@ -0,0 +1,26 @@
+{
+ "description": "NPM package json metadata",
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "version": { "type": "string" },
+ "dist": {
+ "type": "object",
+ "properties": {
+ "tarball": { "type": "string" },
+ "shasum": { "type": "string" }
+ },
+ "additionalProperties": true,
+ "required": [
+ "tarball",
+ "shasum"
+ ]
+ }
+ },
+ "additionalProperties": true,
+ "required": [
+ "name",
+ "version",
+ "dist"
+ ]
+}
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 3f7f929f766..ef6479f8be2 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -23,7 +23,7 @@
= form_tag personal_access_token_import_github_path, method: :post do
.form-group
%label.label-bold= _('Personal Access Token')
- = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' }
+ = text_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' }
%span.form-text.text-muted
= import_github_personal_access_token_message
diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml
index cbe9a860210..be2be7288f8 100644
--- a/app/views/jira_connect/subscriptions/index.html.haml
+++ b/app/views/jira_connect/subscriptions/index.html.haml
@@ -9,20 +9,9 @@
= link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in'
%main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto
- - if current_user.blank? && @subscriptions.empty?
- .jira-connect-app-body.gl-px-5.gl-text-center
- %h2= s_('JiraService|GitLab for Jira Configuration')
- %p= s_('JiraService|Sign in to GitLab.com to get started.')
+ .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
- .gl-mt-7
- = external_link _('Sign in to GitLab'), jira_connect_users_path, class: "btn gl-button btn-confirm js-jira-connect-sign-in"
-
- .gl-mt-7
- %p= s_('Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).')
- - else
- .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
-
- %p.jira-connect-app-body.gl-px-5.gl-mt-7.gl-font-base.gl-text-center
+ %p.jira-connect-app-body.gl-px-5.gl-font-base.gl-text-center.gl-mx-auto
%strong= s_('Integrations|Browser limitations')
- browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
- firefox_link_start = browser_link_start.html_safe % { url: 'https://www.mozilla.org/en-US/firefox/' }
diff --git a/app/views/layouts/_published_experiments.html.haml b/app/views/layouts/_published_experiments.html.haml
new file mode 100644
index 00000000000..717e1e633d2
--- /dev/null
+++ b/app/views/layouts/_published_experiments.html.haml
@@ -0,0 +1,4 @@
+= javascript_tag(nonce: content_security_policy_nonce) do
+ :plain
+ gl = window.gl || {};
+ gl.experiments = #{raw ApplicationExperiment.published_experiments.reject { |name, data| data[:excluded] }.to_json};
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 899bf65de48..26e3d9b3b92 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -16,4 +16,5 @@
= render 'layouts/img_loader'
+ = render 'layouts/published_experiments'
= yield :scripts_body
diff --git a/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml b/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml
new file mode 100644
index 00000000000..74515438af2
--- /dev/null
+++ b/app/views/projects/commit/_multiple_signatures_signature_badge.html.haml
@@ -0,0 +1,6 @@
+- title = capture do
+ = html_escape(_('This commit was signed with %{strong_open}multiple%{strong_close} signatures.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
+
+- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless' }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 2fbaa5812c0..86cac7c8376 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,5 +1,4 @@
- add_page_startup_api_call discussions_path(@issue)
-- add_page_startup_api_call notes_url
- @gfm_form = true
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index c3b1d22a909..ae39bd60233 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -246,6 +246,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:clusters_integrations_check_prometheus_health
+ :worker_name: Clusters::Integrations::CheckPrometheusHealthWorker
+ :feature_category: :incident_management
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:container_expiration_policy
:worker_name: ContainerExpirationPolicyWorker
:feature_category: :container_registry
@@ -1087,15 +1096,6 @@
:idempotent:
:tags:
- :needs_own_queue
-- :name: incident_management:clusters_integrations_check_prometheus_health
- :worker_name: Clusters::Integrations::CheckPrometheusHealthWorker
- :feature_category: :incident_management
- :has_external_dependencies: true
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 2
- :idempotent: true
- :tags: []
- :name: incident_management:incident_management_add_severity_system_note
:worker_name: IncidentManagement::AddSeveritySystemNoteWorker
:feature_category: :incident_management
diff --git a/app/workers/clusters/integrations/check_prometheus_health_worker.rb b/app/workers/clusters/integrations/check_prometheus_health_worker.rb
index 0a583003e6f..0c0d86e975c 100644
--- a/app/workers/clusters/integrations/check_prometheus_health_worker.rb
+++ b/app/workers/clusters/integrations/check_prometheus_health_worker.rb
@@ -12,7 +12,6 @@ module Clusters
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
- queue_namespace :incident_management
feature_category :incident_management
urgency :low
diff --git a/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
index fed469e6dc8..6a1de00ce80 100644
--- a/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
+++ b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
@@ -13,9 +13,8 @@ module DependencyProxy
def perform
DependencyProxy::ImageTtlGroupPolicy.enabled.each do |policy|
- # Technical Debt: change to read_before https://gitlab.com/gitlab-org/gitlab/-/issues/341536
- qualified_blobs = policy.group.dependency_proxy_blobs.active.updated_before(policy.ttl)
- qualified_manifests = policy.group.dependency_proxy_manifests.active.updated_before(policy.ttl)
+ qualified_blobs = policy.group.dependency_proxy_blobs.active.read_before(policy.ttl)
+ qualified_manifests = policy.group.dependency_proxy_manifests.active.read_before(policy.ttl)
enqueue_blob_cleanup_job if expire_artifacts(qualified_blobs, DependencyProxy::Blob)
enqueue_manifest_cleanup_job if expire_artifacts(qualified_manifests, DependencyProxy::Manifest)
diff --git a/config/feature_flags/development/create_project_namespace_on_project_create.yml b/config/feature_flags/development/create_project_namespace_on_project_create.yml
new file mode 100644
index 00000000000..3fbf929ca2e
--- /dev/null
+++ b/config/feature_flags/development/create_project_namespace_on_project_create.yml
@@ -0,0 +1,8 @@
+---
+name: create_project_namespace_on_project_create
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70972
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344954
+milestone: '14.5'
+type: development
+group: group::workspace
+default_enabled: false
diff --git a/config/feature_flags/development/rate_limiter_safe_increment.yml b/config/feature_flags/development/multiple_gpg_signatures.yml
index 16e8f2fe7c7..3b9b8d0a465 100644
--- a/config/feature_flags/development/rate_limiter_safe_increment.yml
+++ b/config/feature_flags/development/multiple_gpg_signatures.yml
@@ -1,8 +1,8 @@
---
-name: rate_limiter_safe_increment
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73343
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285352
+name: multiple_gpg_signatures
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74095
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345261
milestone: '14.5'
type: development
-group: group::project management
+group: group::source code
default_enabled: false
diff --git a/config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml b/config/feature_flags/development/packages_npm_abbreviated_metadata.yml
index d45b8d71a20..422051fa2fd 100644
--- a/config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml
+++ b/config/feature_flags/development/packages_npm_abbreviated_metadata.yml
@@ -1,8 +1,8 @@
---
-name: linear_group_plans_preloaded_ancestor_scopes
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70685
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341349
-milestone: '14.4'
+name: packages_npm_abbreviated_metadata
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73639
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344827
+milestone: '14.5'
type: development
-group: group::access
+group: group::package
default_enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index debaa1e6a82..16e792487d3 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -591,6 +591,9 @@ Settings.cron_jobs['batched_background_migrations_worker']['job_class'] = 'Datab
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances'] ||= Settingslogic.new({})
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['cron'] ||= '* 0/15 * * *'
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['job_class'] = 'Issues::RescheduleStuckIssueRebalancesWorker'
+Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker']['cron'] ||= '0 * * * *'
+Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker']['job_class'] = 'Clusters::Integrations::CheckPrometheusHealthWorker'
Gitlab.ee do
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({})
diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb
index 883824cc16f..5af8cf52656 100644
--- a/config/initializers/postgres_partitioning.rb
+++ b/config/initializers/postgres_partitioning.rb
@@ -35,8 +35,4 @@ unless Gitlab.jh?
])
end
-begin
- Gitlab::Database::Partitioning.sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP']
-rescue ActiveRecord::ActiveRecordError, PG::Error
- # ignore - happens when Rake tasks yet have to create a database, e.g. for testing
-end
+Gitlab::Database::Partitioning.sync_partitions_ignore_db_error
diff --git a/data/deprecations/templates/example.yml b/data/deprecations/templates/example.yml
index ebacb31645e..fb164247cb6 100644
--- a/data/deprecations/templates/example.yml
+++ b/data/deprecations/templates/example.yml
@@ -6,7 +6,7 @@
# and what types we expect those attribute values to be.
#
# For more information please refer to the handbook documentation here:
-# {{LINK TBD}}
+# https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations
#
# Please delete this line and above before submitting your merge request.
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 95d956fb402..fa19775a571 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -157,16 +157,17 @@ class Gitlab::Seeder::CycleAnalytics
end
def create_new_vsm_project
+ namespace = FactoryBot.create(
+ :group,
+ name: "Value Stream Management Group #{suffix}",
+ path: "vsmg-#{suffix}"
+ )
project = FactoryBot.create(
:project,
name: "Value Stream Management Project #{suffix}",
path: "vsmp-#{suffix}",
creator: admin,
- namespace: FactoryBot.create(
- :group,
- name: "Value Stream Management Group #{suffix}",
- path: "vsmg-#{suffix}"
- )
+ namespace: namespace
)
project.create_repository
diff --git a/db/migrate/20211028132247_create_packages_npm_metadata.rb b/db/migrate/20211028132247_create_packages_npm_metadata.rb
new file mode 100644
index 00000000000..cbe5429fca1
--- /dev/null
+++ b/db/migrate/20211028132247_create_packages_npm_metadata.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreatePackagesNpmMetadata < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ create_table :packages_npm_metadata, id: false do |t|
+ t.references :package, primary_key: true, default: nil, index: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint
+ t.jsonb :package_json, default: {}, null: false
+
+ t.check_constraint 'char_length(package_json::text) < 20000'
+ end
+ end
+ end
+
+ def down
+ with_lock_retries do
+ drop_table :packages_npm_metadata
+ end
+ end
+end
diff --git a/db/migrate/20211105125756_add_read_at_to_dependency_proxy_manifests.rb b/db/migrate/20211105125756_add_read_at_to_dependency_proxy_manifests.rb
new file mode 100644
index 00000000000..a594674f470
--- /dev/null
+++ b/db/migrate/20211105125756_add_read_at_to_dependency_proxy_manifests.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddReadAtToDependencyProxyManifests < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :dependency_proxy_manifests, :read_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
+ end
+end
diff --git a/db/migrate/20211105125813_add_read_at_to_dependency_proxy_blobs.rb b/db/migrate/20211105125813_add_read_at_to_dependency_proxy_blobs.rb
new file mode 100644
index 00000000000..1808a541498
--- /dev/null
+++ b/db/migrate/20211105125813_add_read_at_to_dependency_proxy_blobs.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddReadAtToDependencyProxyBlobs < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :dependency_proxy_blobs, :read_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
+ end
+end
diff --git a/db/migrate/20211108203248_update_dependency_proxy_indexes_with_read_at.rb b/db/migrate/20211108203248_update_dependency_proxy_indexes_with_read_at.rb
new file mode 100644
index 00000000000..ac0f0ddca17
--- /dev/null
+++ b/db/migrate/20211108203248_update_dependency_proxy_indexes_with_read_at.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class UpdateDependencyProxyIndexesWithReadAt < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ NEW_BLOB_INDEX = 'index_dependency_proxy_blobs_on_group_id_status_read_at_id'
+ OLD_BLOB_INDEX = 'index_dependency_proxy_blobs_on_group_id_status_and_id'
+
+ NEW_MANIFEST_INDEX = 'index_dependency_proxy_manifests_on_group_id_status_read_at_id'
+ OLD_MANIFEST_INDEX = 'index_dependency_proxy_manifests_on_group_id_status_and_id'
+
+ def up
+ add_concurrent_index :dependency_proxy_blobs, [:group_id, :status, :read_at, :id], name: NEW_BLOB_INDEX
+ add_concurrent_index :dependency_proxy_manifests, [:group_id, :status, :read_at, :id], name: NEW_MANIFEST_INDEX
+
+ remove_concurrent_index_by_name :dependency_proxy_blobs, OLD_BLOB_INDEX
+ remove_concurrent_index_by_name :dependency_proxy_manifests, OLD_MANIFEST_INDEX
+ end
+
+ def down
+ add_concurrent_index :dependency_proxy_blobs, [:group_id, :status, :id], name: OLD_BLOB_INDEX
+ add_concurrent_index :dependency_proxy_manifests, [:group_id, :status, :id], name: OLD_MANIFEST_INDEX
+
+ remove_concurrent_index_by_name :dependency_proxy_blobs, NEW_BLOB_INDEX
+ remove_concurrent_index_by_name :dependency_proxy_manifests, NEW_MANIFEST_INDEX
+ end
+end
diff --git a/db/schema_migrations/20211028132247 b/db/schema_migrations/20211028132247
new file mode 100644
index 00000000000..ab8fa3b55eb
--- /dev/null
+++ b/db/schema_migrations/20211028132247
@@ -0,0 +1 @@
+50a5c8af2cde1ae79d627f70d3b266488f76f76b481aefca8516db5360cfa843 \ No newline at end of file
diff --git a/db/schema_migrations/20211105125756 b/db/schema_migrations/20211105125756
new file mode 100644
index 00000000000..842187d9ae2
--- /dev/null
+++ b/db/schema_migrations/20211105125756
@@ -0,0 +1 @@
+13cf3d164d541df48b6d14d7cc1953113476ba8ea5975d7d0c5f84098e2e0e61 \ No newline at end of file
diff --git a/db/schema_migrations/20211105125813 b/db/schema_migrations/20211105125813
new file mode 100644
index 00000000000..449c3d95d6a
--- /dev/null
+++ b/db/schema_migrations/20211105125813
@@ -0,0 +1 @@
+a48f62bed7e4c4a0e69acd3b340065317aff71602e696970276a4e443f1dcabf \ No newline at end of file
diff --git a/db/schema_migrations/20211108203248 b/db/schema_migrations/20211108203248
new file mode 100644
index 00000000000..4f8c570b627
--- /dev/null
+++ b/db/schema_migrations/20211108203248
@@ -0,0 +1 @@
+a2556a3d8b21e59caa6cbf7f83d621fef391904d0c13c77c0e5da713a580b4c9 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a109953a827..669d2549dd8 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -13237,7 +13237,8 @@ CREATE TABLE dependency_proxy_blobs (
file_store integer,
file_name character varying NOT NULL,
file text NOT NULL,
- status smallint DEFAULT 0 NOT NULL
+ status smallint DEFAULT 0 NOT NULL,
+ read_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE SEQUENCE dependency_proxy_blobs_id_seq
@@ -13286,6 +13287,7 @@ CREATE TABLE dependency_proxy_manifests (
digest text NOT NULL,
content_type text,
status smallint DEFAULT 0 NOT NULL,
+ read_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT check_079b293a7b CHECK ((char_length(file) <= 255)),
CONSTRAINT check_167a9a8a91 CHECK ((char_length(content_type) <= 255)),
CONSTRAINT check_c579e3f586 CHECK ((char_length(file_name) <= 255)),
@@ -17199,6 +17201,12 @@ CREATE SEQUENCE packages_maven_metadata_id_seq
ALTER SEQUENCE packages_maven_metadata_id_seq OWNED BY packages_maven_metadata.id;
+CREATE TABLE packages_npm_metadata (
+ package_id bigint NOT NULL,
+ package_json jsonb DEFAULT '{}'::jsonb NOT NULL,
+ CONSTRAINT chk_rails_e5cbc301ae CHECK ((char_length((package_json)::text) < 20000))
+);
+
CREATE TABLE packages_nuget_dependency_link_metadata (
dependency_link_id bigint NOT NULL,
target_framework text NOT NULL,
@@ -23524,6 +23532,9 @@ ALTER TABLE ONLY packages_helm_file_metadata
ALTER TABLE ONLY packages_maven_metadata
ADD CONSTRAINT packages_maven_metadata_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY packages_npm_metadata
+ ADD CONSTRAINT packages_npm_metadata_pkey PRIMARY KEY (package_id);
+
ALTER TABLE ONLY packages_nuget_dependency_link_metadata
ADD CONSTRAINT packages_nuget_dependency_link_metadata_pkey PRIMARY KEY (dependency_link_id);
@@ -25637,13 +25648,13 @@ CREATE UNIQUE INDEX index_dep_prox_manifests_on_group_id_file_name_and_status ON
CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON dependency_proxy_blobs USING btree (group_id, file_name);
-CREATE INDEX index_dependency_proxy_blobs_on_group_id_status_and_id ON dependency_proxy_blobs USING btree (group_id, status, id);
+CREATE INDEX index_dependency_proxy_blobs_on_group_id_status_read_at_id ON dependency_proxy_blobs USING btree (group_id, status, read_at, id);
CREATE INDEX index_dependency_proxy_blobs_on_status ON dependency_proxy_blobs USING btree (status);
CREATE INDEX index_dependency_proxy_group_settings_on_group_id ON dependency_proxy_group_settings USING btree (group_id);
-CREATE INDEX index_dependency_proxy_manifests_on_group_id_status_and_id ON dependency_proxy_manifests USING btree (group_id, status, id);
+CREATE INDEX index_dependency_proxy_manifests_on_group_id_status_read_at_id ON dependency_proxy_manifests USING btree (group_id, status, read_at, id);
CREATE INDEX index_dependency_proxy_manifests_on_status ON dependency_proxy_manifests USING btree (status);
@@ -30779,6 +30790,9 @@ ALTER TABLE ONLY atlassian_identities
ALTER TABLE ONLY serverless_domain_cluster
ADD CONSTRAINT fk_rails_c09009dee1 FOREIGN KEY (pages_domain_id) REFERENCES pages_domains(id) ON DELETE CASCADE;
+ALTER TABLE ONLY packages_npm_metadata
+ ADD CONSTRAINT fk_rails_c0e5fce6f3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY labels
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6e387cdec4a..41781fa8680 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3509,31 +3509,6 @@ Input type: `MergeRequestSetSubscriptionInput`
| <a id="mutationmergerequestsetsubscriptionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationmergerequestsetsubscriptionmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
-### `Mutation.mergeRequestSetWip`
-
-WARNING:
-**Deprecated** in 13.12.
-Use mergeRequestSetDraft.
-
-Input type: `MergeRequestSetWipInput`
-
-#### Arguments
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| <a id="mutationmergerequestsetwipclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationmergerequestsetwipiid"></a>`iid` | [`String!`](#string) | IID of the merge request to mutate. |
-| <a id="mutationmergerequestsetwipprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the merge request to mutate is in. |
-| <a id="mutationmergerequestsetwipwip"></a>`wip` | [`Boolean!`](#boolean) | Whether or not to set the merge request as a draft. |
-
-#### Fields
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| <a id="mutationmergerequestsetwipclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
-| <a id="mutationmergerequestsetwiperrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
-| <a id="mutationmergerequestsetwipmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
-
### `Mutation.mergeRequestUpdate`
Update attributes of a merge request.
@@ -11583,7 +11558,6 @@ Maven metadata.
| <a id="mergerequestusernotescount"></a>`userNotesCount` | [`Int`](#int) | User notes count of the merge request. |
| <a id="mergerequestuserpermissions"></a>`userPermissions` | [`MergeRequestPermissions!`](#mergerequestpermissions) | Permissions for the current user on the resource. |
| <a id="mergerequestweburl"></a>`webUrl` | [`String`](#string) | Web URL of the merge request. |
-| <a id="mergerequestworkinprogress"></a>`workInProgress` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 13.12. Use `draft`. |
#### Fields with arguments
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 9458fe650bc..2d0a24ea60b 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -365,8 +365,7 @@ include: '.gitlab-ci-production.yml'
#### `include:file`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53903) in GitLab 11.7.
-> - Including multiple files from the same project [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26793) in GitLab 13.6. [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/271560) in GitLab 13.8.
+> Including multiple files from the same project [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26793) in GitLab 13.6. [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/271560) in GitLab 13.8.
To include files from another private project on the same GitLab instance,
use `include:file`. You can use `include:file` in combination with `include:project` only.
@@ -451,8 +450,6 @@ include:
#### `include:template`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53445) in GitLab 11.7.
-
Use `include:template` to include [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
**Keyword type**: Global keyword.
@@ -643,8 +640,7 @@ job2:
**Additional details**:
-You might need to use single quotes (`'`) or double quotes (`"`) when using
-[special characters in `script`](script.md#use-special-characters-with-script).
+- When you use [these special characters in `script`](script.md#use-special-characters-with-script), you must use single quotes (`'`) or double quotes (`"`) .
**Related topics**:
@@ -680,8 +676,8 @@ job:
**Additional details**:
-Scripts you specify in `before_script` are concatenated with any scripts you specify
-in the main [`script`](#script). The combined scripts execute together in a single shell.
+- Scripts you specify in `before_script` are concatenated with any scripts you specify
+ in the main [`script`](#script). The combined scripts execute together in a single shell.
**Related topics**:
@@ -799,7 +795,7 @@ job4:
Use the `.pre` stage to make a job run at the start of a pipeline. `.pre` is
always the first stage in a pipeline. User-defined stages execute after `.pre`.
-You do not need to define `.pre` in [`stages`](#stages).
+You do not have to define `.pre` in [`stages`](#stages).
You must have a job in at least one stage other than `.pre` or `.post`.
@@ -834,7 +830,7 @@ job2:
Use the `.post` stage to make a job run at the end of a pipeline. `.post`
is always the last stage in a pipeline. User-defined stages execute before `.post`.
-You do not need to define `.post` in [`stages`](#stages).
+You do not have to define `.post` in [`stages`](#stages).
You must have a job in at least one stage other than `.pre` or `.post`.
@@ -865,8 +861,6 @@ job2:
### `extends`
-> Introduced in GitLab 11.3.
-
Use `extends` to reuse configuration sections. It's an alternative to [YAML anchors](yaml_specific_features.md#anchors)
and is a little more flexible and readable. You can use `extends` to reuse configuration
from [included configuration files](#use-extends-and-include-together).
@@ -1356,7 +1350,7 @@ pipeline based on branch names or pipeline types.
| `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
| `tags` | When the Git reference for a pipeline is a tag. |
| `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#authentication-tokens). |
- | `web` | For pipelines created by using **Run pipeline** button in the GitLab UI, from the project's **CI/CD > Pipelines** section. |
+ | `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **CI/CD > Pipelines** section. |
**Example of `only:refs` and `except:refs`**:
@@ -1440,8 +1434,6 @@ deploy:
#### `only:changes` / `except:changes`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/19232) in GitLab 11.4.
-
Use the `changes` keyword with `only` to run a job, or with `except` to skip a job,
when a Git push event modifies a file.
@@ -2043,7 +2035,7 @@ Use `environment` to define the [environment](../environments/index.md) that a j
**Possible inputs**: The name of the environment the job deploys to, in one of these
formats:
-- Plain text, including letters, digits, spaces and these characters: `-`, `_`, `/`, `$`, `{`, `}`.
+- Plain text, including letters, digits, spaces, and these characters: `-`, `_`, `/`, `$`, `{`, `}`.
- CI/CD variables, including predefined, secure, or variables defined in the
`.gitlab-ci.yml` file. You can't use variables defined in a `script` section.
@@ -2072,7 +2064,7 @@ Common environment names are `qa`, `staging`, and `production`, but you can use
**Possible inputs**: The name of the environment the job deploys to, in one of these
formats:
-- Plain text, including letters, digits, spaces and these characters: `-`, `_`, `/`, `$`, `{`, `}`.
+- Plain text, including letters, digits, spaces, and these characters: `-`, `_`, `/`, `$`, `{`, `}`.
- CI/CD variables, including predefined, secure, or variables defined in the
`.gitlab-ci.yml` file. You can't use variables defined in a `script` section.
@@ -2124,7 +2116,7 @@ environment.
**Additional details**:
-See [`environment:action`](#environmentaction) for more details and an example.
+- See [`environment:action`](#environmentaction) for more details and an example.
#### `environment:action`
@@ -2392,7 +2384,7 @@ cache-job:
**Additional details**:
-- If you use **Windows Batch** to run your shell scripts you need to replace
+- If you use **Windows Batch** to run your shell scripts you must replace
`$` with `%`. For example: `key: %CI_COMMIT_REF_SLUG%`
- The `cache:key` value can't contain:
@@ -2446,9 +2438,11 @@ these files changes, a new cache key is computed and a new cache is created. Any
job runs that use the same `Gemfile.lock` and `package.json` with `cache:key:files`
use the new cache, instead of rebuilding the dependencies.
-**Additional details**: The cache `key` is a SHA computed from the most recent commits
-that changed each listed file. If neither file is changed in any commits, the
-fallback key is `default`.
+**Additional details**:
+
+- The cache `key` is a SHA computed from the most recent commits
+that changed each listed file.
+ If neither file is changed in any commits, the fallback key is `default`.
##### `cache:key:prefix`
@@ -2485,8 +2479,9 @@ If a branch changes `Gemfile.lock`, that branch has a new SHA checksum for `cach
A new cache key is generated, and a new cache is created for that key. If `Gemfile.lock`
is not found, the prefix is added to `default`, so the key in the example would be `rspec-default`.
-**Additional details**: If no file in `cache:key:files` is changed in any commits,
-the prefix is added to the `default` key.
+**Additional details**:
+
+- If no file in `cache:key:files` is changed in any commits, the prefix is added to the `default` key.
#### `cache:untracked`
@@ -2552,7 +2547,7 @@ This example stores the cache whether or not the job fails or succeeds.
To change the upload and download behavior of a cache, use the `cache:policy` keyword.
By default, the job downloads the cache when the job starts, and uploads changes
-to the cache when the job ends. This is the `pull-push` policy (default).
+to the cache when the job ends. This caching style is the `pull-push` policy (default).
To set a job to only download the cache when the job starts, but never upload changes
when the job finishes, use `cache:policy:pull`.
@@ -2766,7 +2761,7 @@ time is not defined, it defaults to the
To override the expiration date and protect artifacts from being automatically deleted:
-- Use the **Keep** button on the job page.
+- Select **Keep** on the job page.
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
`expire_in` to `never`.
@@ -2871,7 +2866,7 @@ job:
---
-If you use **Windows Batch** to run your shell scripts you need to replace
+If you use **Windows Batch** to run your shell scripts you must replace
`$` with `%`:
```yaml
@@ -2882,7 +2877,7 @@ job:
- binaries/
```
-If you use **Windows PowerShell** to run your shell scripts you need to replace
+If you use **Windows PowerShell** to run your shell scripts you must replace
`$` with `$env:`:
```yaml
@@ -2900,9 +2895,8 @@ link outside it. You can use Wildcards that use [glob](https://en.wikipedia.org/
patterns and:
- In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620),
-[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
-- In GitLab Runner 12.10 and earlier,
-[`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
+ [`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
+- In GitLab Runner 12.10 and earlier, [`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
To restrict which jobs a specific job fetches artifacts from, see [dependencies](#dependencies).
@@ -2983,9 +2977,6 @@ artifacts:
#### `artifacts:reports`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2.
-> - Requires GitLab Runner 11.2 and above.
-
Use [`artifacts:reports`](#artifactsreports) to:
- Collect test reports, code quality reports, and security reports from jobs.
@@ -3041,9 +3032,7 @@ requests and the pipeline view. It's also used to provide data for security dash
##### `artifacts:reports:browser_performance` **(PREMIUM)**
-> - Introduced in GitLab 11.5.
-> - Requires GitLab Runner 11.5 and above.
-> - [Name changed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) from `artifacts:reports:performance` in GitLab 14.0.
+> [Name changed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) from `artifacts:reports:performance` in GitLab 14.0.
The `browser_performance` report collects [Browser Performance Testing metrics](../../user/project/merge_requests/browser_performance_testing.md)
as artifacts.
@@ -3064,8 +3053,7 @@ dashboards.
##### `artifacts:reports:cobertura`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9.
-> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9.
The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
The collected Cobertura coverage reports upload to GitLab as an artifact
@@ -3076,9 +3064,7 @@ third party ports for other languages like JavaScript, Python, Ruby, and so on.
##### `artifacts:reports:codequality`
-> - Introduced in GitLab 11.5.
-> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2.
-> - Requires GitLab Runner 11.5 and above.
+> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2.
The `codequality` report collects [Code Quality issues](../../user/project/merge_requests/code_quality.md)
as artifacts.
@@ -3087,9 +3073,6 @@ The collected Code Quality report uploads to GitLab as an artifact and is summar
##### `artifacts:reports:container_scanning` **(ULTIMATE)**
-> - Introduced in GitLab 11.5.
-> - Requires GitLab Runner 11.5 and above.
-
The `container_scanning` report collects [Container Scanning vulnerabilities](../../user/application_security/container_scanning/index.md)
as artifacts.
@@ -3110,9 +3093,6 @@ requests and the pipeline view. It's also used to provide data for security dash
##### `artifacts:reports:dast` **(ULTIMATE)**
-> - Introduced in GitLab 11.5.
-> - Requires GitLab Runner 11.5 and above.
-
The `dast` report collects [DAST vulnerabilities](../../user/application_security/dast/index.md)
as artifacts.
@@ -3121,9 +3101,6 @@ dashboards.
##### `artifacts:reports:dependency_scanning` **(ULTIMATE)**
-> - Introduced in GitLab 11.5.
-> - Requires GitLab Runner 11.5 and above.
-
The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](../../user/application_security/dependency_scanning/index.md)
as artifacts.
@@ -3132,15 +3109,14 @@ dashboards.
##### `artifacts:reports:dotenv`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9.
-> - Requires GitLab Runner 11.5 and later.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9.
The `dotenv` report collects a set of environment variables as artifacts.
The collected variables are registered as runtime-created variables of the job,
which is useful to [set dynamic environment URLs after a job finishes](../environments/index.md#set-dynamic-environment-urls-after-a-job-finishes).
-There are a couple of exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv#rules):
+The exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv#rules) are:
- The variable key can contain only letters, digits, and underscores (`_`).
- The maximum size of the `.env` file is 5 KB.
@@ -3154,9 +3130,6 @@ There are a couple of exceptions to the [original dotenv rules](https://github.c
##### `artifacts:reports:junit`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2.
-> - Requires GitLab Runner 11.2 and above.
-
The `junit` report collects [JUnit report format XML files](https://www.ibm.com/docs/en/adfz/developer-for-zos/14.1.0?topic=formats-junit-xml-format)
as artifacts. Although JUnit was originally developed in Java, there are many
third party ports for other
@@ -3179,21 +3152,20 @@ rspec:
The collected Unit test reports upload to GitLab as an artifact and display in merge requests.
If the JUnit tool you use exports to multiple XML files, specify
-multiple test report paths within a single job to
+multiple test report paths in a single job to
concatenate them into a single file. Use a filename pattern (`junit: rspec-*.xml`),
an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a
combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`).
##### `artifacts:reports:license_scanning` **(ULTIMATE)**
-> - Introduced in GitLab 12.8.
-> - Requires GitLab Runner 11.5 and above.
+> Introduced in GitLab 12.8.
The `license_scanning` report collects [Licenses](../../user/compliance/license_compliance/index.md)
as artifacts.
-The License Compliance report uploads to GitLab as an artifact and displays automatically in merge requests and the pipeline view, and provide data for security
-dashboards.
+The License Compliance report uploads to GitLab as an artifact and displays automatically
+in merge requests and the pipeline view. The report provides data for security dashboards.
##### `artifacts:reports:load_performance` **(PREMIUM)**
@@ -3208,8 +3180,6 @@ shown in merge requests automatically.
##### `artifacts:reports:metrics` **(PREMIUM)**
-> Introduced in GitLab 11.10.
-
The `metrics` report collects [Metrics](../metrics_reports.md)
as artifacts.
@@ -3218,7 +3188,6 @@ The collected Metrics report uploads to GitLab as an artifact and displays in me
##### `artifacts:reports:requirements` **(ULTIMATE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2859) in GitLab 13.1.
-> - Requires GitLab Runner 11.5 and above.
The `requirements` report collects `requirements.json` files as artifacts.
@@ -3228,9 +3197,7 @@ marked as Satisfied.
##### `artifacts:reports:sast`
-> - Introduced in GitLab 11.5.
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/2098) from GitLab Ultimate to GitLab Free in 13.3.
-> - Requires GitLab Runner 11.5 and above.
The `sast` report collects [SAST vulnerabilities](../../user/application_security/sast/index.md)
as artifacts.
@@ -3302,7 +3269,7 @@ failure.
1. `on_success` (default): Upload artifacts only when the job succeeds.
1. `on_failure`: Upload artifacts only when the job fails.
-1. `always`: Always upload artifacts. Useful, for example, when
+1. `always`: Always upload artifacts. For example, when
[uploading artifacts](../unit_test_reports.md#viewing-junit-screenshots-on-gitlab) required to
troubleshoot failing tests.
@@ -3394,8 +3361,6 @@ to select a specific site profile and scanner profile.
### `retry`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3515) in GitLab 11.5, you can control which failures to retry on.
-
Use `retry` to configure how many times a job is retried if it fails.
If not defined, defaults to `0` and jobs do not retry.
@@ -3513,8 +3478,6 @@ test:
### `parallel`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/21480) in GitLab 11.5.
-
Use `parallel` to run a job multiple times in parallel in a single pipeline.
Multiple runners must exist, or a single runner must be configured to run multiple jobs concurrently.
@@ -3987,7 +3950,7 @@ This keyword must be used with `secrets:vault`.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28321) in GitLab 13.4 and GitLab Runner 13.4.
-Use `secrets:vault` to specify secrets provided by a [Hashicorp Vault](https://www.vaultproject.io/).
+Use `secrets:vault` to specify secrets provided by a [HashiCorp Vault](https://www.vaultproject.io/).
**Keyword type**: Job keyword. You can use it only as part of a job.
diff --git a/doc/development/experiment_guide/gitlab_experiment.md b/doc/development/experiment_guide/gitlab_experiment.md
index 1cd3fefb7cf..af4512dcde0 100644
--- a/doc/development/experiment_guide/gitlab_experiment.md
+++ b/doc/development/experiment_guide/gitlab_experiment.md
@@ -92,7 +92,7 @@ end
```
When this code executes, the experiment is run, a variant is assigned, and (if within a
-controller or view) a `window.gon.experiment.pill_color` object will be available in the
+controller or view) a `window.gl.experiments.pill_color` object will be available in the
client layer, with details like:
- The assigned variant.
@@ -522,14 +522,14 @@ shared example: [tracks assignment and records the subject](https://gitlab.com/g
This is in flux as of GitLab 13.10, and can't be documented just yet.
-Any experiment that's been run in the request lifecycle surfaces in `window.gon.experiment`,
+Any experiment that's been run in the request lifecycle surfaces in and `window.gl.experiments`,
and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0)
so you can use it when resolving some concepts around experimentation in the client layer.
### Use experiments in Vue
With the `gitlab-experiment` component, you can define slots that match the name of the
-variants pushed to `window.gon.experiment`. For example, if we alter the `pill_color`
+variants pushed to `window.gl.experiments`. For example, if we alter the `pill_color`
experiment to just use the default variants of `control` and `candidate` like so:
```ruby
@@ -587,7 +587,51 @@ For example, the Vue component for the previously-defined `pill_color` experimen
```
NOTE:
-When there is no experiment data in the `window.gon.experiment` object for the given experiment name, the `control` slot will be used, if it exists.
+When there is no experiment data in the `window.gl.experiments` object for the given experiment name, the `control` slot will be used, if it exists.
+
+## Test with Jest
+
+### Stub Helpers
+
+You can stub experiments using the `stubExperiments` helper defined in `spec/frontend/__helpers__/experimentation_helper.js`.
+
+```javascript
+import { stubExperiments } from 'helpers/experimentation_helper';
+import { getExperimentData } from '~/experimentation/utils';
+
+describe('when my_experiment is enabled', () => {
+ beforeEach(() => {
+ stubExperiments({ my_experiment: 'candidate' });
+ });
+
+ it('sets the correct data', () => {
+ expect(getExperimentData('my_experiment')).toEqual({ experiment: 'my_experiment', variant: 'candidate' });
+ });
+});
+```
+
+NOTE:
+This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself:
+
+```javascript
+desribe('tests that care about global state', () => {
+ const originalObjects = [];
+
+ beforeEach(() => {
+ // For backwards compatibility for now, we're using both window.gon & window.gl
+ originalObjects.push(window.gon, window.gl);
+ });
+
+ afterEach(() => {
+ [window.gon, window.gl] = originalObjects;
+ });
+
+ it('stubs experiment in fresh global state', () => {
+ stubExperiment({ my_experiment: 'candidate' });
+ // ...
+ });
+})
+```
## Notes on feature flags
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index d249d36f159..dd2d3d8bf32 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -588,8 +588,6 @@ class like so:
```ruby
class MyMigration < Gitlab::Database::Migration[1.0]
- include Gitlab::Database::MigrationHelpers
-
disable_ddl_transaction!
INDEX_NAME = 'index_name'
@@ -633,8 +631,6 @@ be used with a name option. For example:
```ruby
class MyMigration < Gitlab::Database::Migration[1.0]
- include Gitlab::Database::MigrationHelpers
-
INDEX_NAME = 'index_name'
def up
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 433c00571be..454be0e52f2 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -208,6 +208,15 @@ Then, you can run `npm publish` either locally or by using GitLab CI/CD.
- **GitLab CI/CD:** Set an `NPM_TOKEN` [CI/CD variable](../../../ci/variables/index.md)
under your project's **Settings > CI/CD > Variables**.
+## Working with private registries
+
+When working with private repositories, you may want to configure additional settings to ensure a secure communication channel:
+
+```shell
+# Force npm to always require authentication when accessing the registry, even for GET requests.
+npm config set always-auth true
+```
+
## Package naming convention
When you use the [instance-level endpoint](#use-the-gitlab-endpoint-for-npm-packages), only the packages with names in the format of `@scope/package-name` are available.
@@ -363,6 +372,10 @@ This rule has a different impact depending on the package name:
This aligns with npmjs.org's behavior. However, npmjs.org does not ever let you publish
the same version more than once, even if it has been deleted.
+## `package.json` limitations
+
+You can't publish a package if its `package.json` file exceeds 20,000 characters.
+
## Install a package
npm packages are commonly-installed by using the `npm` or `yarn` commands
@@ -427,22 +440,29 @@ and use your organization's URL. The name is case-sensitive and must match the n
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
```
-### npm dependencies metadata
+### npm metadata
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11867) in GitLab Premium 12.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
-
-In GitLab 12.6 and later, packages published to the Package Registry expose the following attributes to the npm client:
-
-- name
-- version
-- dist-tags
-- dependencies
- - dependencies
- - devDependencies
- - bundleDependencies
- - peerDependencies
- - deprecated
+> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/330929) in GitLab 14.5.
+
+The GitLab Package Registry exposes the following attributes to the npm client.
+These are similar to the [abbreviated metadata format](https://github.com/npm/registry/blob/9e368cf6aaca608da5b2c378c0d53f475298b916/docs/responses/package-metadata.md#abbreviated-metadata-format):
+
+- `name`
+- `versions`
+ - `name`
+ - `version`
+ - `deprecated`
+ - `dependencies`
+ - `devDependencies`
+ - `bundleDependencies`
+ - `peerDependencies`
+ - `bin`
+ - `directories`
+ - `dist`
+ - `engines`
+ - `_hasShrinkwrap`
## Add npm distribution tags
@@ -579,6 +599,10 @@ root namespace and therefore cannot be published again using the same name.
This is also true even if the prior published package shares the same name,
but not the version.
+#### Package JSON file is too large
+
+Make sure that your `package.json` file does not [exceed `20,000` characters](#packagejson-limitations).
+
### `npm publish` returns `npm ERR! 500 Internal Server Error - PUT`
This is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/238950) in GitLab
diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md
index eaab1933b79..d155f91e40b 100644
--- a/doc/user/project/integrations/custom_issue_tracker.md
+++ b/doc/user/project/integrations/custom_issue_tracker.md
@@ -4,31 +4,40 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Custom issue tracker service **(FREE)**
+# Custom issue tracker **(FREE)**
-Use a custom issue tracker that is not in the integration list.
+You can integrate an [external issue tracker](../../../integration/external-issue-tracker.md)
+with GitLab. If your preferred issue tracker is not listed in the
+[integrations list](../../../integration/external-issue-tracker.md#integration),
+you can enable a custom issue tracker.
+
+After you enable the custom issue tracker, a link to the issue tracker displays
+on the left sidebar in your project.
+
+![Custom issue tracker link](img/custom_issue_tracker_v14_5.png)
+
+## Enable a custom issue tracker
To enable a custom issue tracker in a project:
-1. Go to the [Integrations page](overview.md#accessing-integrations).
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > Integrations**.
1. Select **Custom issue tracker**.
1. Select the checkbox under **Enable integration**.
1. Fill in the required fields:
- **Project URL**: The URL to view all the issues in the custom issue tracker.
- **Issue URL**: The URL to view an issue in the custom issue tracker. The URL must contain `:id`.
- GitLab replaces `:id` with the issue number (for example,
- `https://customissuetracker.com/project-name/:id`, which becomes `https://customissuetracker.com/project-name/123`).
+ GitLab replaces `:id` with the issue number (for example,
+ `https://customissuetracker.com/project-name/:id`, which becomes
+ `https://customissuetracker.com/project-name/123`).
- **New issue URL**:
<!-- The line below was originally added in January 2018: https://gitlab.com/gitlab-org/gitlab/-/commit/778b231f3a5dd42ebe195d4719a26bf675093350 -->
- **This URL is not used and removal is planned in a future release.**
- Enter any URL here.
- For more information, see [issue 327503](https://gitlab.com/gitlab-org/gitlab/-/issues/327503).
-
-1. Select **Save changes** or optionally select **Test settings**.
+ **This URL is not used and an [issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/327503) to remove it.**
+ Enter any URL.
-After you configure and enable the custom issue tracker service, a link appears on the GitLab
-project pages. This link takes you to the custom issue tracker.
+1. Optional. Select **Test settings**.
+1. Select **Save changes**.
## Reference issues in a custom issue tracker
diff --git a/doc/user/project/integrations/img/custom_issue_tracker_v14_5.png b/doc/user/project/integrations/img/custom_issue_tracker_v14_5.png
new file mode 100644
index 00000000000..e316a2acc39
--- /dev/null
+++ b/doc/user/project/integrations/img/custom_issue_tracker_v14_5.png
Binary files differ
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index 5fe3e5be4c0..510fcd61cb6 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -207,7 +207,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip
service_desk_email:
enabled: true
address: "project_contact+%{key}@example.com"
- user: "project_support@example.com"
+ user: "project_contact@example.com"
password: "[REDACTED]"
host: "imap.gmail.com"
port: 993
@@ -224,7 +224,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip
```ruby
gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com"
- gitlab_rails['service_desk_email_email'] = "project_support@gmail.com"
+ gitlab_rails['service_desk_email_email'] = "project_contact@gmail.com"
gitlab_rails['service_desk_email_password'] = "[REDACTED]"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_idle_timeout'] = 60
diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb
index d6e006df976..c62947722a9 100644
--- a/lib/api/concerns/packages/npm_endpoints.rb
+++ b/lib/api/concerns/packages/npm_endpoints.rb
@@ -121,7 +121,9 @@ module API
not_found!('Packages') if packages.empty?
- present ::Packages::Npm::PackagePresenter.new(package_name, packages),
+ include_metadata = Feature.enabled?(:packages_npm_abbreviated_metadata, project)
+
+ present ::Packages::Npm::PackagePresenter.new(package_name, packages, include_metadata: include_metadata),
with: ::API::Entities::NpmPackage
end
end
diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb
index f01ce22a46d..6cc29d63919 100644
--- a/lib/bulk_imports/ndjson_pipeline.rb
+++ b/lib/bulk_imports/ndjson_pipeline.rb
@@ -13,7 +13,7 @@ module BulkImports
relation_hash, relation_index = data
relation_definition = import_export_config.top_relation_tree(relation)
- deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash|
+ relation_object = deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash|
relation_factory.create(
relation_index: relation_index,
relation_sym: key.to_sym,
@@ -25,6 +25,9 @@ module BulkImports
excluded_keys: import_export_config.relation_excluded_keys(key)
)
end
+
+ relation_object.assign_attributes(portable_class_sym => portable)
+ relation_object
end
def load(_, object)
@@ -94,6 +97,10 @@ module BulkImports
def members_mapper
@members_mapper ||= BulkImports::UsersMapper.new(context: context)
end
+
+ def portable_class_sym
+ portable.class.to_s.downcase.to_sym
+ end
end
end
end
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index ef8fd9504f7..3db2f1295f9 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -79,28 +79,6 @@ module Gitlab
increment(key, options[:scope]) > threshold_value
end
- # Increments the given cache key and increments the value by 1 with the
- # expiration interval defined in `.rate_limits`.
- #
- # @param key [Symbol] Key attribute registered in `.rate_limits`
- # @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
- #
- # @return [Integer] incremented value
- def increment(key, scope)
- return safe_increment(key, scope) if Feature.enabled?(:rate_limiter_safe_increment, default_enabled: :yaml)
-
- value = 0
- interval_value = interval(key)
-
- ::Gitlab::Redis::RateLimiting.with do |redis|
- cache_key = action_key(key, scope)
- value = redis.incr(cache_key)
- redis.expire(cache_key, interval_value) if value == 1
- end
-
- value
- end
-
# Increments a cache key that is based on the current time and interval.
# So that when time passes to the next interval, the key changes and the count starts again from 0.
#
@@ -110,7 +88,7 @@ module Gitlab
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
#
# @return [Integer] incremented value
- def safe_increment(key, scope)
+ def increment(key, scope)
interval_value = interval(key)
period_key, time_elapsed_in_period = Time.now.to_i.divmod(interval_value)
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index dca5326f270..c0ba2c58a41 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -355,6 +355,7 @@ packages_dependency_links: :gitlab_main
packages_events: :gitlab_main
packages_helm_file_metadata: :gitlab_main
packages_maven_metadata: :gitlab_main
+packages_npm_metadata: :gitlab_main
packages_nuget_dependency_link_metadata: :gitlab_main
packages_nuget_metadata: :gitlab_main
packages_package_file_build_infos: :gitlab_main
diff --git a/lib/gitlab/database/partitioning.rb b/lib/gitlab/database/partitioning.rb
index 866477c46da..1343354715a 100644
--- a/lib/gitlab/database/partitioning.rb
+++ b/lib/gitlab/database/partitioning.rb
@@ -31,6 +31,12 @@ module Gitlab
registered_tables.merge(tables)
end
+ def sync_partitions_ignore_db_error
+ sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP']
+ rescue ActiveRecord::ActiveRecordError, PG::Error
+ # ignore - happens when Rake tasks yet have to create a database, e.g. for testing
+ end
+
def sync_partitions(models_to_sync = registered_for_sync)
Gitlab::AppLogger.info(message: 'Syncing dynamic postgres partitions')
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index da1a3efd562..5afdcc0bd4c 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -20,6 +20,7 @@ module Gitlab
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'
NoRepository = Class.new(::Gitlab::Git::BaseError)
+ RepositoryExists = Class.new(::Gitlab::Git::BaseError)
InvalidRepository = Class.new(::Gitlab::Git::BaseError)
InvalidBlobName = Class.new(::Gitlab::Git::BaseError)
InvalidRef = Class.new(::Gitlab::Git::BaseError)
@@ -101,6 +102,8 @@ module Gitlab
def create_repository
wrapped_gitaly_errors do
gitaly_repository_client.create_repository
+ rescue GRPC::AlreadyExists => e
+ raise RepositoryExists, e.message
end
end
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 1abbd6dc45b..9a6317e2b76 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -48,7 +48,7 @@ module Gitlab
if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
- clear_memoization(:verified_signature)
+ clear_memoization(:gpg_signatures)
end
yield gpg_key
@@ -56,16 +56,7 @@ module Gitlab
end
def verified_signature
- strong_memoize(:verified_signature) { gpgme_signature }
- end
-
- def gpgme_signature
- GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
- # Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-foss/issues/54932
- break verified_signature
- end
- rescue GPGME::Error
- nil
+ gpg_signatures.first
end
def create_cached_signature!
@@ -77,6 +68,24 @@ module Gitlab
end
end
+ def gpg_signatures
+ strong_memoize(:gpg_signatures) do
+ signatures = []
+
+ GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
+ signatures << verified_signature
+ end
+
+ signatures
+ rescue GPGME::Error
+ []
+ end
+ end
+
+ def multiple_signatures?
+ gpg_signatures.size > 1
+ end
+
def attributes(gpg_key)
user_infos = user_infos(gpg_key)
verification_status = verification_status(gpg_key)
@@ -93,6 +102,7 @@ module Gitlab
end
def verification_status(gpg_key)
+ return :multiple_signatures if multiple_signatures? && Feature.enabled?(:multiple_gpg_signatures, @commit.project, default_enabled: :yaml)
return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
return :unverified unless verified_signature&.valid?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index bb95f06cec1..f24c6060c8f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -23326,6 +23326,9 @@ msgstr ""
msgid "No forks are available to you."
msgstr ""
+msgid "No group provided"
+msgstr ""
+
msgid "No grouping"
msgstr ""
@@ -23377,6 +23380,9 @@ msgstr ""
msgid "No members found"
msgstr ""
+msgid "No memberships found"
+msgstr ""
+
msgid "No merge requests found"
msgstr ""
@@ -24442,6 +24448,9 @@ msgstr ""
msgid "Package type must be Maven"
msgstr ""
+msgid "Package type must be NPM"
+msgstr ""
+
msgid "Package type must be NuGet"
msgstr ""
@@ -35112,6 +35121,9 @@ msgstr ""
msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request."
msgstr ""
+msgid "This commit was signed with %{strong_open}multiple%{strong_close} signatures."
+msgstr ""
+
msgid "This commit was signed with a %{strong_open}verified%{strong_close} signature and the committer email is verified to belong to the same user."
msgstr ""
@@ -39464,6 +39476,9 @@ msgstr ""
msgid "You do not have permission to access dora metrics."
msgstr ""
+msgid "You do not have permission to approve a member"
+msgstr ""
+
msgid "You do not have permission to leave this %{namespaceType}."
msgstr ""
@@ -41767,6 +41782,9 @@ msgstr ""
msgid "starts on %{timebox_start_date}"
msgstr ""
+msgid "structure is too large"
+msgstr ""
+
msgid "stuck"
msgstr ""
diff --git a/qa/chemlab-library-gitlab.gemspec b/qa/chemlab-library-gitlab.gemspec
index 34a55ba8927..9af4a650d98 100644
--- a/qa/chemlab-library-gitlab.gemspec
+++ b/qa/chemlab-library-gitlab.gemspec
@@ -19,4 +19,5 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']
spec.add_runtime_dependency 'chemlab', '~> 0.9'
+ spec.add_runtime_dependency 'zeitwerk', '~> 2.4'
end
diff --git a/qa/lib/gitlab.rb b/qa/lib/gitlab.rb
index 4418e51facb..8c33071633d 100644
--- a/qa/lib/gitlab.rb
+++ b/qa/lib/gitlab.rb
@@ -1,31 +1,14 @@
# frozen_string_literal: true
require 'chemlab/library'
+require 'zeitwerk'
+
+loader = Zeitwerk::Loader.new
+loader.push_dir(__dir__)
+loader.ignore("#{__dir__}/gitlab/**/*.stub.rb") # ignore page stubs
+loader.setup
# Chemlab Page Libraries for GitLab
module Gitlab
include Chemlab::Library
-
- module Page
- module Main
- autoload :Login, 'gitlab/page/main/login'
- autoload :SignUp, 'gitlab/page/main/sign_up'
- end
-
- module Subscriptions
- autoload :New, 'gitlab/page/subscriptions/new'
- end
-
- module Admin
- autoload :Dashboard, 'gitlab/page/admin/dashboard'
- autoload :Subscription, 'gitlab/page/admin/subscription'
- end
-
- module Group
- module Settings
- autoload :Billing, 'gitlab/page/group/settings/billing'
- autoload :UsageQuotas, 'gitlab/page/group/settings/usage_quotas'
- end
- end
- end
end
diff --git a/qa/lib/gitlab/page/main/welcome.rb b/qa/lib/gitlab/page/main/welcome.rb
new file mode 100644
index 00000000000..a2df1da61c9
--- /dev/null
+++ b/qa/lib/gitlab/page/main/welcome.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Main
+ class Welcome < Chemlab::Page
+ path '/users/sign_up/welcome'
+
+ button :get_started_button
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/main/welcome.stub.rb b/qa/lib/gitlab/page/main/welcome.stub.rb
new file mode 100644
index 00000000000..a10e697bcbf
--- /dev/null
+++ b/qa/lib/gitlab/page/main/welcome.stub.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Main
+ module Welcome
+ # @note Defined as +button :get_started_button+
+ # Clicks +get_started_button+
+ def get_started_button
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Welcome.perform do |welcome|
+ # expect(welcome.get_started_button_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def get_started_button_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Welcome.perform do |welcome|
+ # expect(welcome).to be_get_started_button
+ # end
+ # @return [Boolean] true if the +get_started_button+ element is present on the page
+ def get_started_button?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index f0aa351bee0..cf528b414c0 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe Dashboard::TodosController do
create(:issue, project: project, assignees: [user])
group_2 = create(:group)
group_2.add_owner(user)
- project_2 = create(:project)
+ project_2 = create(:project, namespace: user.namespace)
project_2.add_developer(user)
merge_request_2 = create(:merge_request, source_project: project_2)
create(:todo, project: project, author: author, user: user, target: merge_request_2)
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 3d7636b1f30..5b1c6777523 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -86,10 +86,11 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:maintainer) { true }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
before do
- project.add_maintainer(user)
+ project.add_maintainer(user) if maintainer
sign_in(user)
end
@@ -383,8 +384,9 @@ RSpec.describe Projects::MergeRequests::DiffsController do
end
context 'when the user cannot view the merge request' do
+ let(:maintainer) { false }
+
before do
- project.team.truncate
diff_for_path(old_path: existing_path, new_path: existing_path)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 438fc2f2106..e2fc867db44 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -10,7 +10,8 @@ RSpec.describe Projects::MergeRequestsController do
let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) }
let(:user) { project.owner }
- let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_maintainer_to_push: false) }
+ let(:merge_request_source_project) { project }
before do
sign_in(user)
@@ -2073,8 +2074,6 @@ RSpec.describe Projects::MergeRequestsController do
end
describe 'POST #rebase' do
- let(:viewer) { user }
-
def post_rebase
post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request }
end
@@ -2085,7 +2084,7 @@ RSpec.describe Projects::MergeRequestsController do
context 'successfully' do
it 'enqeues a RebaseWorker' do
- expect_rebase_worker_for(viewer)
+ expect_rebase_worker_for(user)
post_rebase
@@ -2108,17 +2107,17 @@ RSpec.describe Projects::MergeRequestsController do
context 'with a forked project' do
let(:forked_project) { fork_project(project, fork_owner, repository: true) }
let(:fork_owner) { create(:user) }
+ let(:merge_request_source_project) { forked_project }
- before do
- project.add_developer(fork_owner)
+ context 'user cannot push to source branch' do
+ before do
+ project.add_developer(fork_owner)
- merge_request.update!(source_project: forked_project)
- forked_project.add_reporter(user)
- end
+ forked_project.add_reporter(user)
+ end
- context 'user cannot push to source branch' do
it 'returns 404' do
- expect_rebase_worker_for(viewer).never
+ expect_rebase_worker_for(user).never
post_rebase
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index a25c597edb2..baf500c2b57 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -499,13 +499,12 @@ RSpec.describe RegistrationsController do
expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}")
end
- it 'sets the username and caller_id in the context' do
+ it 'sets the caller_id in the context' do
expect(controller).to receive(:create).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current)
- .to include('meta.user' => base_user_params[:username],
- 'meta.caller_id' => 'RegistrationsController#create')
+ .to include('meta.caller_id' => 'RegistrationsController#create')
end
subject
diff --git a/spec/factories/namespaces/project_namespaces.rb b/spec/factories/namespaces/project_namespaces.rb
index ca9fc5f8768..6bf17088741 100644
--- a/spec/factories/namespaces/project_namespaces.rb
+++ b/spec/factories/namespaces/project_namespaces.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :project_namespace, class: 'Namespaces::ProjectNamespace' do
- project
+ association :project, factory: :project, strategy: :build
parent { project.namespace }
visibility_level { project.visibility_level }
name { project.name }
diff --git a/spec/factories/packages/npm/metadata.rb b/spec/factories/packages/npm/metadata.rb
new file mode 100644
index 00000000000..c8acaa10199
--- /dev/null
+++ b/spec/factories/packages/npm/metadata.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :npm_metadatum, class: 'Packages::Npm::Metadatum' do
+ package { association(:npm_package) }
+
+ package_json do
+ {
+ 'name': package.name,
+ 'version': package.version,
+ 'dist': {
+ 'tarball': 'http://localhost/tarball.tgz',
+ 'shasum': '1234567890'
+ }
+ }
+ end
+ end
+end
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 70b53f23508..610a80eb12c 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -115,6 +115,19 @@ RSpec.describe 'GPG signed commits' do
end
end
+ it 'unverified signature: commit contains multiple GPG signatures' do
+ user_1_key
+
+ visit project_commit_path(project, GpgHelpers::MULTIPLE_SIGNATURES_SHA)
+ wait_for_all_requests
+
+ page.find('.gpg-status-box', text: 'Unverified').click
+
+ within '.popover' do
+ expect(page).to have_content "This commit was signed with multiple signatures."
+ end
+ end
+
it 'verified and the gpg user has a gitlab profile' do
user_1_key
@@ -169,7 +182,7 @@ RSpec.describe 'GPG signed commits' do
page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do
- expect(page).to have_content 'This commit was signed with an unverified signature'
+ expect(page).to have_content 'This commit was signed with multiple signatures.'
end
end
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json b/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json
index 3e74dc0a1c2..64969d71250 100644
--- a/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/npm_package_version.json
@@ -36,11 +36,11 @@
".{1,}": { "type": "string" }
}
},
- "deprecated": {
- "type": "object",
- "patternProperties": {
- ".{1,}": { "type": "string" }
- }
- }
+ "deprecated": { "type": "string"},
+ "bin": { "type": "string" },
+ "directories": { "type": "array" },
+ "engines": { "type": "object" },
+ "_hasShrinkwrap": { "type": "boolean" },
+ "additionalProperties": true
}
}
diff --git a/spec/fixtures/packages/npm/payload.json b/spec/fixtures/packages/npm/payload.json
index 664aa636001..5ecb013b9bf 100644
--- a/spec/fixtures/packages/npm/payload.json
+++ b/spec/fixtures/packages/npm/payload.json
@@ -14,7 +14,8 @@
"express":"^4.16.4"
},
"dist":{
- "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
+ "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
+ "tarball":"http://localhost/npm/package.tgz"
}
}
},
diff --git a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
index a6ea8760bd5..bc4a7b3f55a 100644
--- a/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
+++ b/spec/fixtures/packages/npm/payload_with_duplicated_packages.json
@@ -28,7 +28,8 @@
"express":"^4.16.4"
},
"dist":{
- "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
+ "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
+ "tarball":"http://localhost/npm/package.tgz"
}
}
},
diff --git a/spec/frontend/__helpers__/experimentation_helper.js b/spec/frontend/__helpers__/experimentation_helper.js
index 7a2ef61216a..e0156226acc 100644
--- a/spec/frontend/__helpers__/experimentation_helper.js
+++ b/spec/frontend/__helpers__/experimentation_helper.js
@@ -1,5 +1,6 @@
import { merge } from 'lodash';
+// This helper is for specs that use `gitlab/experimentation` module
export function withGonExperiment(experimentKey, value = true) {
let origGon;
@@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) {
window.gon = origGon;
});
}
-// This helper is for specs that use `gitlab-experiment` utilities, which have a different schema that gets pushed via Gon compared to `Experimentation Module`
-export function assignGitlabExperiment(experimentKey, variant) {
- let origGon;
- beforeEach(() => {
- origGon = window.gon;
- window.gon = { experiment: { [experimentKey]: { variant } } };
- });
+// The following helper is for specs that use `gitlab-experiment` utilities,
+// which have a different schema that gets pushed to the frontend compared to
+// the `Experimentation` Module.
+//
+// Usage: stubExperiments({ experiment_feature_flag_name: 'variant_name', ... })
+export function stubExperiments(experiments = {}) {
+ // Deprecated
+ window.gon = window.gon || {};
+ window.gon.experiment = window.gon.experiment || {};
+ // Preferred
+ window.gl = window.gl || {};
+ window.gl.experiments = window.gl.experiemnts || {};
- afterEach(() => {
- window.gon = origGon;
+ Object.entries(experiments).forEach(([name, variant]) => {
+ const experimentData = { experiment: name, variant };
+
+ // Deprecated
+ window.gon.experiment[name] = experimentData;
+ // Preferred
+ window.gl.experiments[name] = experimentData;
});
}
diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js
index e66c31953f6..075fe225ec2 100644
--- a/spec/frontend/boards/components/new_board_button_spec.js
+++ b/spec/frontend/boards/components/new_board_button_spec.js
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { assignGitlabExperiment } from 'helpers/experimentation_helper';
+import { stubExperiments } from 'helpers/experimentation_helper';
import eventHub from '~/boards/eventhub';
const FEATURE = 'prominent_create_board_btn';
@@ -28,7 +28,9 @@ describe('NewBoardButton', () => {
});
describe('control variant', () => {
- assignGitlabExperiment(FEATURE, 'control');
+ beforeAll(() => {
+ stubExperiments({ [FEATURE]: 'control' });
+ });
it('renders nothing', () => {
wrapper = createComponent();
@@ -38,7 +40,9 @@ describe('NewBoardButton', () => {
});
describe('candidate variant', () => {
- assignGitlabExperiment(FEATURE, 'candidate');
+ beforeAll(() => {
+ stubExperiments({ [FEATURE]: 'candidate' });
+ });
it('renders New board button when `candidate` variant', () => {
wrapper = createComponent();
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index e39b761b54d..342b4bfcc50 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -241,18 +241,19 @@ describe('DiffFileHeader component', () => {
});
describe('for any file', () => {
- const otherModes = Object.keys(diffViewerModes).filter((m) => m !== 'mode_changed');
+ const allModes = Object.keys(diffViewerModes).map((m) => [m]);
- it('for mode_changed file mode displays mode changes', () => {
+ it.each(allModes)('for %s file mode displays mode changes', (mode) => {
createComponent({
props: {
diffFile: {
...diffFile,
+ mode_changed: true,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
...diffFile.viewer,
- name: diffViewerModes.mode_changed,
+ name: diffViewerModes[mode],
},
},
},
@@ -260,13 +261,14 @@ describe('DiffFileHeader component', () => {
expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/);
});
- it.each(otherModes.map((m) => [m]))(
+ it.each(allModes.filter((m) => m[0] !== 'mode_changed'))(
'for %s file mode does not display mode changes',
(mode) => {
createComponent({
props: {
diffFile: {
...diffFile,
+ mode_changed: false,
a_mode: 'old-mode',
b_mode: 'new-mode',
viewer: {
diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js
index e9b7d445473..df24828ceb9 100644
--- a/spec/frontend/experimentation/utils_spec.js
+++ b/spec/frontend/experimentation/utils_spec.js
@@ -1,4 +1,4 @@
-import { assignGitlabExperiment } from 'helpers/experimentation_helper';
+import { stubExperiments } from 'helpers/experimentation_helper';
import {
DEFAULT_VARIANT,
CANDIDATE_VARIANT,
@@ -7,15 +7,45 @@ import {
import * as experimentUtils from '~/experimentation/utils';
describe('experiment Utilities', () => {
- const TEST_KEY = 'abc';
+ const ABC_KEY = 'abc';
+ const DEF_KEY = 'def';
+
+ let origGon;
+ let origGl;
+
+ beforeEach(() => {
+ origGon = window.gon;
+ origGl = window.gl;
+ window.gon.experiment = {};
+ window.gl.experiments = {};
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ window.gl = origGl;
+ });
describe('getExperimentData', () => {
+ const ABC_DATA = '_abc_data_';
+ const ABC_DATA2 = '_updated_abc_data_';
+ const DEF_DATA = '_def_data_';
+
describe.each`
- gon | input | output
- ${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${{ variant: '_data_' }}
- ${[]} | ${[TEST_KEY]} | ${undefined}
- `('with input=$input and gon=$gon', ({ gon, input, output }) => {
- assignGitlabExperiment(...gon);
+ gonData | glData | input | output
+ ${[ABC_KEY, ABC_DATA]} | ${[]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[]} | ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[DEF_KEY]} | ${{ experiment: DEF_KEY, variant: DEF_DATA }}
+ ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY, ABC_DATA2]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA2 }}
+ ${[]} | ${[]} | ${[ABC_KEY]} | ${undefined}
+ `('with input=$input, gon=$gonData, & gl=$glData', ({ gonData, glData, input, output }) => {
+ beforeEach(() => {
+ const [gonKey, gonVariant] = gonData;
+ const [glKey, glVariant] = glData;
+
+ if (gonKey) window.gon.experiment[gonKey] = { experiment: gonKey, variant: gonVariant };
+ if (glKey) window.gl.experiments[glKey] = { experiment: glKey, variant: glVariant };
+ });
it(`returns ${output}`, () => {
expect(experimentUtils.getExperimentData(...input)).toEqual(output);
@@ -25,66 +55,47 @@ describe('experiment Utilities', () => {
describe('getAllExperimentContexts', () => {
const schema = TRACKING_CONTEXT_SCHEMA;
- let origGon;
-
- beforeEach(() => {
- origGon = window.gon;
- });
-
- afterEach(() => {
- window.gon = origGon;
- });
it('collects all of the experiment contexts into a single array', () => {
- const experiments = [
- { experiment: 'abc', variant: 'candidate' },
- { experiment: 'def', variant: 'control' },
- { experiment: 'ghi', variant: 'blue' },
- ];
- window.gon = {
- experiment: experiments.reduce((collector, { experiment, variant }) => {
- return { ...collector, [experiment]: { experiment, variant } };
- }, {}),
- };
+ const experiments = { [ABC_KEY]: 'candidate', [DEF_KEY]: 'control', ghi: 'blue' };
+
+ stubExperiments(experiments);
expect(experimentUtils.getAllExperimentContexts()).toEqual(
- experiments.map((data) => ({ schema, data })),
+ Object.entries(experiments).map(([experiment, variant]) => ({
+ schema,
+ data: { experiment, variant },
+ })),
);
});
it('returns an empty array if there are no experiments', () => {
- window.gon.experiment = {};
-
expect(experimentUtils.getAllExperimentContexts()).toEqual([]);
});
-
- it('includes all additional experiment data', () => {
- const experiment = 'experimentWithCustomData';
- const data = { experiment, variant: 'control', color: 'blue', style: 'rounded' };
- window.gon.experiment[experiment] = data;
-
- expect(experimentUtils.getAllExperimentContexts()).toContainEqual({ schema, data });
- });
});
describe('isExperimentVariant', () => {
describe.each`
- gon | input | output
- ${[TEST_KEY, DEFAULT_VARIANT]} | ${[TEST_KEY, DEFAULT_VARIANT]} | ${true}
- ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_variant_name']} | ${true}
- ${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_bogus_name']} | ${false}
- ${[TEST_KEY, '_variant_name']} | ${['boguskey', '_variant_name']} | ${false}
- ${[]} | ${[TEST_KEY, '_variant_name']} | ${false}
- `('with input=$input and gon=$gon', ({ gon, input, output }) => {
- assignGitlabExperiment(...gon);
-
- it(`returns ${output}`, () => {
- expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
- });
- });
+ experiment | variant | input | output
+ ${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true}
+ ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true}
+ ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false}
+ ${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false}
+ ${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false}
+ `(
+ 'with input=$input, experiment=$experiment, variant=$variant',
+ ({ experiment, variant, input, output }) => {
+ it(`returns ${output}`, () => {
+ if (experiment) stubExperiments({ [experiment]: variant });
+
+ expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
+ });
+ },
+ );
});
describe('experiment', () => {
+ const experiment = 'marley';
const useSpy = jest.fn();
const controlSpy = jest.fn();
const trySpy = jest.fn();
@@ -98,49 +109,62 @@ describe('experiment Utilities', () => {
};
describe('when there is no experiment data', () => {
- it('calls control variant', () => {
- experimentUtils.experiment('marley', variants);
+ it('calls the use variant', () => {
+ experimentUtils.experiment(experiment, variants);
expect(useSpy).toHaveBeenCalled();
});
+
+ describe("when 'control' is provided instead of 'use'", () => {
+ it('calls the control variant', () => {
+ experimentUtils.experiment(experiment, { control: controlSpy });
+ expect(controlSpy).toHaveBeenCalled();
+ });
+ });
});
describe('when experiment variant is "control"', () => {
- assignGitlabExperiment('marley', DEFAULT_VARIANT);
+ beforeEach(() => {
+ stubExperiments({ [experiment]: DEFAULT_VARIANT });
+ });
- it('calls the control variant', () => {
- experimentUtils.experiment('marley', variants);
+ it('calls the use variant', () => {
+ experimentUtils.experiment(experiment, variants);
expect(useSpy).toHaveBeenCalled();
});
describe("when 'control' is provided instead of 'use'", () => {
it('calls the control variant', () => {
- experimentUtils.experiment('marley', { control: controlSpy });
+ experimentUtils.experiment(experiment, { control: controlSpy });
expect(controlSpy).toHaveBeenCalled();
});
});
});
describe('when experiment variant is "candidate"', () => {
- assignGitlabExperiment('marley', CANDIDATE_VARIANT);
+ beforeEach(() => {
+ stubExperiments({ [experiment]: CANDIDATE_VARIANT });
+ });
- it('calls the candidate variant', () => {
- experimentUtils.experiment('marley', variants);
+ it('calls the try variant', () => {
+ experimentUtils.experiment(experiment, variants);
expect(trySpy).toHaveBeenCalled();
});
describe("when 'candidate' is provided instead of 'try'", () => {
- it('calls the control variant', () => {
- experimentUtils.experiment('marley', { candidate: candidateSpy });
+ it('calls the candidate variant', () => {
+ experimentUtils.experiment(experiment, { candidate: candidateSpy });
expect(candidateSpy).toHaveBeenCalled();
});
});
});
describe('when experiment variant is "get_up_stand_up"', () => {
- assignGitlabExperiment('marley', 'get_up_stand_up');
+ beforeEach(() => {
+ stubExperiments({ [experiment]: 'get_up_stand_up' });
+ });
it('calls the get-up-stand-up variant', () => {
- experimentUtils.experiment('marley', variants);
+ experimentUtils.experiment(experiment, variants);
expect(getUpStandUpSpy).toHaveBeenCalled();
});
});
@@ -148,14 +172,17 @@ describe('experiment Utilities', () => {
describe('getExperimentVariant', () => {
it.each`
- gon | input | output
- ${{ experiment: { [TEST_KEY]: { variant: DEFAULT_VARIANT } } }} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
- ${{ experiment: { [TEST_KEY]: { variant: CANDIDATE_VARIANT } } }} | ${[TEST_KEY]} | ${CANDIDATE_VARIANT}
- ${{}} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
- `('with input=$input and gon=$gon, returns $output', ({ gon, input, output }) => {
- window.gon = gon;
-
- expect(experimentUtils.getExperimentVariant(...input)).toEqual(output);
- });
+ experiment | variant | input | output
+ ${ABC_KEY} | ${DEFAULT_VARIANT} | ${ABC_KEY} | ${DEFAULT_VARIANT}
+ ${ABC_KEY} | ${CANDIDATE_VARIANT} | ${ABC_KEY} | ${CANDIDATE_VARIANT}
+ ${undefined} | ${undefined} | ${ABC_KEY} | ${DEFAULT_VARIANT}
+ `(
+ 'with input=$input, experiment=$experiment, & variant=$variant; returns $output',
+ ({ experiment, variant, input, output }) => {
+ stubExperiments({ [experiment]: variant });
+
+ expect(experimentUtils.getExperimentVariant(input)).toEqual(output);
+ },
+ );
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index b63236dec82..8e464968453 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -1,12 +1,14 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
+import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
+import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { __ } from '~/locale';
+import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }),
@@ -20,6 +22,8 @@ describe('JiraConnectApp', () => {
const findAlertLink = () => findAlert().findComponent(GlLink);
const findSignInButton = () => wrapper.findComponent(SignInButton);
const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
+ const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ provide, mountFn = shallowMount } = {}) => {
store = createStore();
@@ -36,91 +40,114 @@ describe('JiraConnectApp', () => {
describe('template', () => {
describe.each`
- scenario | usersPath | expectSignInButton | expectNamespaceButton
- ${'user is not signed in'} | ${'/users'} | ${true} | ${false}
- ${'user is signed in'} | ${undefined} | ${false} | ${true}
- `('when $scenario', ({ usersPath, expectSignInButton, expectNamespaceButton }) => {
- beforeEach(() => {
- createComponent({
- provide: {
- usersPath,
- },
+ scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList
+ ${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true}
+ ${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false}
+ ${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true}
+ ${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false}
+ `(
+ 'when $scenario',
+ ({
+ usersPath,
+ expectSignInButton,
+ subscriptions,
+ expectEmptyState,
+ expectNamespaceButton,
+ expectSubscriptionsList,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ usersPath,
+ subscriptions,
+ },
+ });
});
- });
- it('renders sign in button as expected', () => {
- expect(findSignInButton().exists()).toBe(expectSignInButton);
- });
+ it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => {
+ expect(findSignInButton().exists()).toBe(expectSignInButton);
+ });
- it('renders "Add Namespace" button as expected', () => {
- expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
- });
- });
+ it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
+ expect(findEmptyState().exists()).toBe(expectEmptyState);
+ });
- describe('alert', () => {
- it.each`
- message | variant | alertShouldRender
- ${'Test error'} | ${'danger'} | ${true}
- ${'Test notice'} | ${'info'} | ${true}
- ${''} | ${undefined} | ${false}
- ${undefined} | ${undefined} | ${false}
- `(
- 'renders correct alert when message is `$message` and variant is `$variant`',
- async ({ message, alertShouldRender, variant }) => {
- createComponent();
-
- store.commit(SET_ALERT, { message, variant });
- await wrapper.vm.$nextTick();
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(alertShouldRender);
- if (alertShouldRender) {
- expect(alert.isVisible()).toBe(alertShouldRender);
- expect(alert.html()).toContain(message);
- expect(alert.props('variant')).toBe(variant);
- expect(findAlertLink().exists()).toBe(false);
- }
- },
- );
-
- it('hides alert on @dismiss event', async () => {
+ it(`${
+ expectNamespaceButton ? 'renders' : 'does not render'
+ } button to add namespace`, () => {
+ expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
+ });
+
+ it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
+ expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
+ });
+ },
+ );
+ });
+
+ describe('alert', () => {
+ it.each`
+ message | variant | alertShouldRender
+ ${'Test error'} | ${'danger'} | ${true}
+ ${'Test notice'} | ${'info'} | ${true}
+ ${''} | ${undefined} | ${false}
+ ${undefined} | ${undefined} | ${false}
+ `(
+ 'renders correct alert when message is `$message` and variant is `$variant`',
+ async ({ message, alertShouldRender, variant }) => {
createComponent();
- store.commit(SET_ALERT, { message: 'test message' });
+ store.commit(SET_ALERT, { message, variant });
await wrapper.vm.$nextTick();
- findAlert().vm.$emit('dismiss');
- await wrapper.vm.$nextTick();
+ const alert = findAlert();
- expect(findAlert().exists()).toBe(false);
- });
+ expect(alert.exists()).toBe(alertShouldRender);
+ if (alertShouldRender) {
+ expect(alert.isVisible()).toBe(alertShouldRender);
+ expect(alert.html()).toContain(message);
+ expect(alert.props('variant')).toBe(variant);
+ expect(findAlertLink().exists()).toBe(false);
+ }
+ },
+ );
- it('renders link when `linkUrl` is set', async () => {
- createComponent({ mountFn: mount });
+ it('hides alert on @dismiss event', async () => {
+ createComponent();
- store.commit(SET_ALERT, {
- message: __('test message %{linkStart}test link%{linkEnd}'),
- linkUrl: 'https://gitlab.com',
- });
- await wrapper.vm.$nextTick();
+ store.commit(SET_ALERT, { message: 'test message' });
+ await wrapper.vm.$nextTick();
+
+ findAlert().vm.$emit('dismiss');
+ await wrapper.vm.$nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
- const alertLink = findAlertLink();
+ it('renders link when `linkUrl` is set', async () => {
+ createComponent({ mountFn: mount });
- expect(alertLink.exists()).toBe(true);
- expect(alertLink.text()).toContain('test link');
- expect(alertLink.attributes('href')).toBe('https://gitlab.com');
+ store.commit(SET_ALERT, {
+ message: __('test message %{linkStart}test link%{linkEnd}'),
+ linkUrl: 'https://gitlab.com',
});
+ await wrapper.vm.$nextTick();
- describe('when alert is set in localStoage', () => {
- it('renders alert on mount', () => {
- createComponent();
+ const alertLink = findAlertLink();
- const alert = findAlert();
+ expect(alertLink.exists()).toBe(true);
+ expect(alertLink.text()).toContain('test link');
+ expect(alertLink.attributes('href')).toBe('https://gitlab.com');
+ });
- expect(alert.exists()).toBe(true);
- expect(alert.html()).toContain('error message');
- });
+ describe('when alert is set in localStoage', () => {
+ it('renders alert on mount', () => {
+ createComponent();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+ expect(alert.html()).toContain('error message');
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
index 32b43765843..4e4a2b58600 100644
--- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
@@ -1,12 +1,15 @@
-import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
+import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
+
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { reloadPage } from '~/jira_connect/subscriptions/utils';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils');
@@ -15,11 +18,13 @@ describe('SubscriptionsList', () => {
let wrapper;
let store;
- const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
+ const createComponent = () => {
store = createStore();
- wrapper = mountFn(SubscriptionsList, {
- provide,
+ wrapper = mount(SubscriptionsList, {
+ provide: {
+ subscriptions: [mockSubscription],
+ },
store,
});
};
@@ -28,28 +33,28 @@ describe('SubscriptionsList', () => {
wrapper.destroy();
});
- const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findGlTable = () => wrapper.findComponent(GlTable);
- const findUnlinkButton = () => findGlTable().findComponent(GlButton);
+ const findUnlinkButton = () => wrapper.findComponent(GlButton);
const clickUnlinkButton = () => findUnlinkButton().trigger('click');
describe('template', () => {
- it('renders GlEmptyState when subscriptions is empty', () => {
+ beforeEach(() => {
createComponent();
+ });
+
+ it('renders "name" cell correctly', () => {
+ const groupItemNames = wrapper.findAllComponents(GroupItemName);
+ expect(groupItemNames.wrappers).toHaveLength(1);
- expect(findGlEmptyState().exists()).toBe(true);
- expect(findGlTable().exists()).toBe(false);
+ const item = groupItemNames.at(0);
+ expect(item.props('group')).toBe(mockSubscription.group);
});
- it('renders GlTable when subscriptions are present', () => {
- createComponent({
- provide: {
- subscriptions: [mockSubscription],
- },
- });
+ it('renders "created at" cell correctly', () => {
+ const timeAgoTooltips = wrapper.findAllComponents(TimeagoTooltip);
+ expect(timeAgoTooltips.wrappers).toHaveLength(1);
- expect(findGlEmptyState().exists()).toBe(false);
- expect(findGlTable().exists()).toBe(true);
+ const item = timeAgoTooltips.at(0);
+ expect(item.props('time')).toBe(mockSubscription.created_at);
});
});
@@ -57,12 +62,7 @@ describe('SubscriptionsList', () => {
let removeSubscriptionSpy;
beforeEach(() => {
- createComponent({
- mountFn: mount,
- provide: {
- subscriptions: [mockSubscription],
- },
- });
+ createComponent();
removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue();
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
index be15e4df66d..0fb0d5b0b68 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -46,7 +46,7 @@ describe('Wip', () => {
is_new_mr_data: true,
};
- describe('handleRemoveWIP', () => {
+ describe('handleRemoveDraft', () => {
it('should make a request to service and handle response', (done) => {
const vm = createComponent();
@@ -59,7 +59,7 @@ describe('Wip', () => {
}),
);
- vm.handleRemoveWIP();
+ vm.handleRemoveDraft();
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
@@ -84,7 +84,7 @@ describe('Wip', () => {
expect(el.innerText).toContain('This merge request is still a draft.');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge');
- expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain(
+ expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain(
'Mark as ready',
);
});
@@ -93,7 +93,7 @@ describe('Wip', () => {
vm.mr.removeWIPPath = '';
Vue.nextTick(() => {
- expect(el.querySelector('.js-remove-wip')).toEqual(null);
+ expect(el.querySelector('.js-remove-draft')).toEqual(null);
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index 4d0e5e71f27..fc760f5c5be 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -15,7 +15,7 @@ describe('getStateKey', () => {
branchMissing: false,
commitsCount: 2,
hasConflicts: false,
- workInProgress: false,
+ draft: false,
};
const bound = getStateKey.bind(context);
@@ -49,9 +49,9 @@ describe('getStateKey', () => {
expect(bound()).toEqual('unresolvedDiscussions');
- context.workInProgress = true;
+ context.draft = true;
- expect(bound()).toEqual('workInProgress');
+ expect(bound()).toEqual('draft');
context.onlyAllowMergeIfPipelineSucceeds = true;
context.isPipelineFailed = true;
@@ -99,7 +99,7 @@ describe('getStateKey', () => {
branchMissing: false,
commitsCount: 2,
hasConflicts: false,
- workInProgress: false,
+ draft: false,
};
const bound = getStateKey.bind(context);
diff --git a/spec/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
deleted file mode 100644
index fae9c4f7fe0..00000000000
--- a/spec/graphql/mutations/merge_requests/set_wip_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Mutations::MergeRequests::SetWip do
- let(:merge_request) { create(:merge_request) }
- let(:user) { create(:user) }
-
- subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
-
- specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
-
- describe '#resolve' do
- let(:wip) { true }
- let(:mutated_merge_request) { subject[:merge_request] }
-
- subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, wip: wip) }
-
- it_behaves_like 'permission level for merge request mutation is correctly verified'
-
- context 'when the user can update the merge request' do
- before do
- merge_request.project.add_developer(user)
- end
-
- it 'returns the merge request as a wip' do
- expect(mutated_merge_request).to eq(merge_request)
- expect(mutated_merge_request).to be_work_in_progress
- expect(subject[:errors]).to be_empty
- end
-
- it 'returns errors merge request could not be updated' do
- # Make the merge request invalid
- merge_request.allow_broken = true
- merge_request.update!(source_project: nil)
-
- expect(subject[:errors]).not_to be_empty
- end
-
- context 'when passing wip as false' do
- let(:wip) { false }
-
- it 'removes `wip` from the title' do
- merge_request.update!(title: "WIP: working on it")
-
- expect(mutated_merge_request).not_to be_work_in_progress
- end
-
- it 'does not do anything if the title did not start with wip' do
- expect(mutated_merge_request).not_to be_work_in_progress
- end
- end
- end
- end
-end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index bc3ccb0d9ba..b17b7c32289 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
notes discussions user_permissions id iid title title_html description
description_html state created_at updated_at source_project target_project
project project_id source_project_id target_project_id source_branch
- target_branch work_in_progress draft merge_when_pipeline_succeeds diff_head_sha
+ target_branch draft merge_when_pipeline_succeeds diff_head_sha
merge_commit_sha user_notes_count user_discussions_count should_remove_source_branch
diff_refs diff_stats diff_stats_summary
force_remove_source_branch
diff --git a/spec/graphql/types/mutation_type_spec.rb b/spec/graphql/types/mutation_type_spec.rb
index c1a5c93c85b..95d835c88cf 100644
--- a/spec/graphql/types/mutation_type_spec.rb
+++ b/spec/graphql/types/mutation_type_spec.rb
@@ -3,14 +3,6 @@
require 'spec_helper'
RSpec.describe Types::MutationType do
- it 'is expected to have the deprecated MergeRequestSetWip' do
- field = get_field('MergeRequestSetWip')
-
- expect(field).to be_present
- expect(field.deprecation_reason).to be_present
- expect(field.resolver).to eq(Mutations::MergeRequests::SetWip)
- end
-
it 'is expected to have the MergeRequestSetDraft' do
expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft)
end
diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
index 7d156c2c3df..c5197fb29d9 100644
--- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
@@ -111,6 +111,7 @@ RSpec.describe BulkImports::NdjsonPipeline do
context = double(portable: group, current_user: user, import_export_config: config, bulk_import: import_double, entity: entity_double)
allow(subject).to receive(:import_export_config).and_return(config)
allow(subject).to receive(:context).and_return(context)
+ relation_object = double
expect(Gitlab::ImportExport::Group::RelationFactory)
.to receive(:create)
@@ -124,6 +125,8 @@ RSpec.describe BulkImports::NdjsonPipeline do
user: user,
excluded_keys: nil
)
+ .and_return(relation_object)
+ expect(relation_object).to receive(:assign_attributes).with(group: group)
subject.transform(context, data)
end
diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb
index 685bf223f9c..7c07c4f7b3d 100644
--- a/spec/lib/bulk_imports/projects/stage_spec.rb
+++ b/spec/lib/bulk_imports/projects/stage_spec.rb
@@ -27,7 +27,8 @@ RSpec.describe BulkImports::Projects::Stage do
describe '#pipelines' do
it 'list all the pipelines with their stage number, ordered by stage' do
- expect(subject.pipelines).to eq(pipelines)
+ expect(subject.pipelines & pipelines).to eq(pipelines)
+ expect(subject.pipelines.last.last).to eq(BulkImports::Common::Pipelines::EntityFinisher)
end
end
end
diff --git a/spec/lib/gitlab/application_rate_limiter_spec.rb b/spec/lib/gitlab/application_rate_limiter_spec.rb
index 0ce7ae8d3aa..c74bcf8d678 100644
--- a/spec/lib/gitlab/application_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/application_rate_limiter_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::ApplicationRateLimiter do
subject { described_class }
- describe '.throttled?' do
+ describe '.throttled?', :clean_gitlab_redis_rate_limiting do
let(:rate_limits) do
{
test_action: {
@@ -56,136 +56,51 @@ RSpec.describe Gitlab::ApplicationRateLimiter do
end
end
- context 'when rate_limiter_safe_increment is disabled' do
- let(:redis) { double('redis') }
- let(:key) { rate_limits.keys[0] }
+ shared_examples 'throttles based on key and scope' do
+ let(:start_time) { Time.current.beginning_of_hour }
- before do
- allow(Gitlab::Redis::RateLimiting).to receive(:with).and_yield(redis)
-
- stub_feature_flags(rate_limiter_safe_increment: false)
- end
-
- shared_examples 'action rate limiter' do
- it 'increases the throttle count and sets the expiration time' do
- expect(redis).to receive(:incr).with(cache_key).and_return(1)
- expect(redis).to receive(:expire).with(cache_key, 120)
-
- expect(subject.throttled?(key, scope: scope)).to be_falsy
+ it 'returns true when threshold is exceeded' do
+ travel_to(start_time) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
end
- it 'returns true if the key is throttled' do
- expect(redis).to receive(:incr).with(cache_key).and_return(2)
- expect(redis).not_to receive(:expire)
+ travel_to(start_time + 1.minute) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(true)
- expect(subject.throttled?(key, scope: scope)).to be_truthy
- end
-
- context 'when throttling is disabled' do
- it 'returns false and does not set expiration time' do
- expect(redis).not_to receive(:incr)
- expect(redis).not_to receive(:expire)
-
- expect(subject.throttled?(key, scope: scope, threshold: 0)).to be_falsy
- end
+ # Assert that it does not affect other actions or scope
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
+ expect(subject.throttled?(:test_action, scope: [user])).to eq(false)
end
end
- context 'when the key is an array of only ActiveRecord models' do
- let(:scope) { [user, project] }
+ it 'returns false when interval has elapsed' do
+ travel_to(start_time) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
- let(:cache_key) do
- "application_rate_limiter:test_action:user:#{user.id}:project:#{project.id}"
+ # another_action has a threshold of 3 so we simulate 2 requests
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
end
- it_behaves_like 'action rate limiter'
-
- context 'when a scope attribute is nil' do
- let(:scope) { [user, nil] }
-
- let(:cache_key) do
- "application_rate_limiter:test_action:user:#{user.id}"
- end
-
- it_behaves_like 'action rate limiter'
- end
- end
-
- context 'when the key is a combination of ActiveRecord models and strings' do
- let(:project) { create(:project, :public, :repository) }
- let(:commit) { project.repository.commit }
- let(:path) { 'app/controllers/groups_controller.rb' }
- let(:scope) { [project, commit, path] }
-
- let(:cache_key) do
- "application_rate_limiter:test_action:project:#{project.id}:commit:#{commit.sha}:#{path}"
- end
-
- it_behaves_like 'action rate limiter'
-
- context 'when a scope attribute is nil' do
- let(:scope) { [project, commit, nil] }
-
- let(:cache_key) do
- "application_rate_limiter:test_action:project:#{project.id}:commit:#{commit.sha}"
- end
+ travel_to(start_time + 2.minutes) do
+ expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
- it_behaves_like 'action rate limiter'
+ # Assert that another_action has its own interval that hasn't elapsed
+ expect(subject.throttled?(:another_action, scope: scope)).to eq(true)
end
end
end
- context 'when rate_limiter_safe_increment is enabled', :clean_gitlab_redis_rate_limiting do
- before do
- stub_feature_flags(rate_limiter_safe_increment: true)
- end
-
- shared_examples 'throttles based on key and scope' do
- let(:start_time) { Time.current.beginning_of_hour }
-
- it 'returns true when threshold is exceeded' do
- travel_to(start_time) do
- expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
- end
-
- travel_to(start_time + 1.minute) do
- expect(subject.throttled?(:test_action, scope: scope)).to eq(true)
-
- # Assert that it does not affect other actions or scope
- expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
- expect(subject.throttled?(:test_action, scope: [user])).to eq(false)
- end
- end
-
- it 'returns false when interval has elapsed' do
- travel_to(start_time) do
- expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
-
- # another_action has a threshold of 3 so we simulate 2 requests
- expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
- expect(subject.throttled?(:another_action, scope: scope)).to eq(false)
- end
-
- travel_to(start_time + 2.minutes) do
- expect(subject.throttled?(:test_action, scope: scope)).to eq(false)
-
- # Assert that another_action has its own interval that hasn't elapsed
- expect(subject.throttled?(:another_action, scope: scope)).to eq(true)
- end
- end
- end
-
- context 'when using ActiveRecord models as scope' do
- let(:scope) { [user, project] }
+ context 'when using ActiveRecord models as scope' do
+ let(:scope) { [user, project] }
- it_behaves_like 'throttles based on key and scope'
- end
+ it_behaves_like 'throttles based on key and scope'
+ end
- context 'when using ActiveRecord models and strings as scope' do
- let(:scope) { [project, 'app/controllers/groups_controller.rb'] }
+ context 'when using ActiveRecord models and strings as scope' do
+ let(:scope) { [project, 'app/controllers/groups_controller.rb'] }
- it_behaves_like 'throttles based on key and scope'
- end
+ it_behaves_like 'throttles based on key and scope'
end
end
diff --git a/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb
index e746451b1b9..f9628849dbf 100644
--- a/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb
@@ -111,11 +111,11 @@ RSpec.describe Gitlab::BackgroundMigration::PopulatePersonalSnippetStatistics do
if with_repo
allow(snippet).to receive(:disk_path).and_return(disk_path(snippet))
+ raw_repository(snippet).create_repository
+
TestEnv.copy_repo(snippet,
bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA)
-
- raw_repository(snippet).create_repository
end
end
end
diff --git a/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb
index 897f5e81372..7884e0d97c0 100644
--- a/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb
@@ -183,11 +183,11 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateProjectSnippetStatistics do
if with_repo
allow(snippet).to receive(:disk_path).and_return(disk_path(snippet))
+ raw_repository(snippet).create_repository
+
TestEnv.copy_repo(snippet,
bare_repo: TestEnv.factory_repo_path_bare,
refs: TestEnv::BRANCH_SHA)
-
- raw_repository(snippet).create_repository
end
end
end
diff --git a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
index cdcc862c376..9d49db1f018 100644
--- a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
@@ -38,7 +38,8 @@ RSpec.describe Gitlab::Database::Count::ReltuplesCountStrategy do
it 'returns nil counts for inherited tables' do
models.each { |model| expect(model).not_to receive(:count) }
- expect(subject).to eq({ Namespace => 3 })
+ # 3 Namespaces as parents for each Project and 3 ProjectNamespaces(for each Project)
+ expect(subject).to eq({ Namespace => 6 })
end
end
diff --git a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
index c2028f8c238..2f261aebf02 100644
--- a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
@@ -47,7 +47,8 @@ RSpec.describe Gitlab::Database::Count::TablesampleCountStrategy do
result = subject
expect(result[Project]).to eq(3)
expect(result[Group]).to eq(1)
- expect(result[Namespace]).to eq(4)
+ # 1-Group, 3 namespaces for each project and 3 project namespaces for each project
+ expect(result[Namespace]).to eq(7)
end
end
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 55102554508..20d5972bd88 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Gitlab::Gpg::Commit do
it 'returns a valid signature' do
verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
allow(GPGME::Crypto).to receive(:new).and_return(crypto)
- allow(crypto).to receive(:verify).and_return(verified_signature)
+ allow(crypto).to receive(:verify).and_yield(verified_signature)
signature = described_class.new(commit).signature
@@ -178,7 +178,7 @@ RSpec.describe Gitlab::Gpg::Commit do
keyid = GpgHelpers::User1.fingerprint.last(16)
verified_signature = double('verified-signature', fingerprint: keyid, valid?: true)
allow(GPGME::Crypto).to receive(:new).and_return(crypto)
- allow(crypto).to receive(:verify).and_return(verified_signature)
+ allow(crypto).to receive(:verify).and_yield(verified_signature)
signature = described_class.new(commit).signature
@@ -194,6 +194,71 @@ RSpec.describe Gitlab::Gpg::Commit do
end
end
+ context 'commit with multiple signatures' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
+
+ let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ let!(:crypto) { instance_double(GPGME::Crypto) }
+
+ before do
+ fake_signature = [
+ GpgHelpers::User1.signed_commit_signature,
+ GpgHelpers::User1.signed_commit_base_data
+ ]
+
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
+ .with(Gitlab::Git::Repository, commit_sha)
+ .and_return(fake_signature)
+ end
+
+ it 'returns an invalid signatures error' do
+ verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
+ allow(GPGME::Crypto).to receive(:new).and_return(crypto)
+ allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature)
+
+ signature = described_class.new(commit).signature
+
+ expect(signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.emails.first,
+ verification_status: 'multiple_signatures'
+ )
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(multiple_gpg_signatures: false)
+ end
+
+ it 'returns an valid signature' do
+ verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
+ allow(GPGME::Crypto).to receive(:new).and_return(crypto)
+ allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature)
+
+ signature = described_class.new(commit).signature
+
+ expect(signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.emails.first,
+ verification_status: 'verified'
+ )
+ end
+ end
+ end
+
context 'commit signed with a subkey' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first }
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 2f38ed58727..f0ba0f0459d 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -425,6 +425,9 @@ RSpec.describe Gitlab::PathRegex do
it { is_expected.not_to match('gitlab.org/') }
it { is_expected.not_to match('/gitlab.org') }
it { is_expected.not_to match('gitlab git') }
+ it { is_expected.not_to match('gitlab?') }
+ it { is_expected.to match('gitlab.org-') }
+ it { is_expected.to match('gitlab.org_') }
end
describe '.project_path_format_regex' do
@@ -437,6 +440,14 @@ RSpec.describe Gitlab::PathRegex do
it { is_expected.not_to match('?gitlab') }
it { is_expected.not_to match('git lab') }
it { is_expected.not_to match('gitlab.git') }
+ it { is_expected.not_to match('gitlab?') }
+ it { is_expected.not_to match('gitlab git') }
+ it { is_expected.to match('gitlab.org') }
+ it { is_expected.to match('gitlab.org-') }
+ it { is_expected.to match('gitlab.org_') }
+ it { is_expected.to match('gitlab.org.') }
+ it { is_expected.not_to match('gitlab.org/') }
+ it { is_expected.not_to match('/gitlab.org') }
end
context 'repository routes' do
diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb
index 94a9a8cde12..d38e842c666 100644
--- a/spec/models/concerns/loaded_in_group_list_spec.rb
+++ b/spec/models/concerns/loaded_in_group_list_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe LoadedInGroupList do
context 'with project namespaces' do
let_it_be(:group1) { create(:group, parent: parent) }
let_it_be(:group2) { create(:group, parent: parent) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'does not include project_namespaces in the count of subgroups' do
expect(found_group.preloaded_subgroup_count).to eq(3)
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index c326b2d6fcd..2467ef541b2 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -566,7 +566,7 @@ RSpec.describe Group do
context 'when project namespace exists in the group' do
let!(:project) { create(:project, group: group) }
- let!(:project_namespace) { create(:project_namespace, project: project) }
+ let!(:project_namespace) { project.project_namespace }
it 'filters out project namespace' do
expect(group.descendants.find_by_id(project_namespace.id)).to be_nil
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 46a22f64591..8f5860c799c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -32,9 +32,10 @@ RSpec.describe Namespace do
describe '#children' do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:project_namespace) { create(:project_namespace, parent: group) }
+ let_it_be(:project_with_namespace) { create(:project, namespace: group) }
it 'excludes project namespaces' do
+ expect(project_with_namespace.project_namespace.parent).to eq(group)
expect(group.children).to match_array([subgroup])
end
end
@@ -239,7 +240,9 @@ RSpec.describe Namespace do
let(:namespace) { build(:project_namespace) }
it 'allows to update path to single char' do
- namespace = create(:project_namespace)
+ project = create(:project)
+ namespace = project.project_namespace
+
namespace.update!(path: 'j')
expect(namespace).to be_valid
@@ -342,9 +345,13 @@ RSpec.describe Namespace do
describe '.without_project_namespaces' do
let_it_be(:user_namespace) { create(:user_namespace) }
- let_it_be(:project_namespace) { create(:project_namespace) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'excludes project namespaces' do
+ expect(project_namespace).not_to be_nil
+ expect(project_namespace.parent).not_to be_nil
+ expect(described_class.all).to include(project_namespace)
expect(described_class.without_project_namespaces).to match_array([namespace, namespace1, namespace2, namespace1sub, namespace2sub, user_namespace, project_namespace.parent])
end
end
@@ -562,7 +569,7 @@ RSpec.describe Namespace do
context 'with project namespaces' do
let_it_be(:project) { create(:project, namespace: parent_group, path: 'some-new-path') }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'does not return project namespace' do
search_result = described_class.search('path')
diff --git a/spec/models/namespaces/project_namespace_spec.rb b/spec/models/namespaces/project_namespace_spec.rb
index f38e8aa85d0..4416c49f1bf 100644
--- a/spec/models/namespaces/project_namespace_spec.rb
+++ b/spec/models/namespaces/project_namespace_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Namespaces::ProjectNamespace, type: :model do
# using delete rather than destroy due to `delete` skipping AR hooks/callbacks
# so it's ensured to work at the DB level. Uses ON DELETE CASCADE on foreign key
let_it_be(:project) { create(:project) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'also deletes the associated project' do
project_namespace.delete
diff --git a/spec/models/packages/npm/metadatum_spec.rb b/spec/models/packages/npm/metadatum_spec.rb
new file mode 100644
index 00000000000..ff8cce5310e
--- /dev/null
+++ b/spec/models/packages/npm/metadatum_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::Metadatum, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:package).inverse_of(:npm_metadatum) }
+ end
+
+ describe 'validations' do
+ describe 'package', :aggregate_failures do
+ it { is_expected.to validate_presence_of(:package) }
+
+ it 'ensure npm package type' do
+ metadatum = build(:npm_metadatum)
+
+ metadatum.package = build(:nuget_package)
+
+ expect(metadatum).not_to be_valid
+ expect(metadatum.errors).to contain_exactly('Package type must be NPM')
+ end
+ end
+
+ describe 'package_json', :aggregate_failures do
+ let(:valid_json) { { 'name' => 'foo', 'version' => 'v1.0', 'dist' => { 'tarball' => 'x', 'shasum' => 'x' } } }
+
+ it { is_expected.to allow_value(valid_json).for(:package_json) }
+ it { is_expected.to allow_value(valid_json.merge('extra-field': { 'foo': 'bar' })).for(:package_json) }
+ it { is_expected.to allow_value(with_dist { |dist| dist.merge('extra-field': 'x') }).for(:package_json) }
+
+ %w[name version dist].each do |field|
+ it { is_expected.not_to allow_value(valid_json.except(field)).for(:package_json) }
+ end
+
+ %w[tarball shasum].each do |field|
+ it { is_expected.not_to allow_value(with_dist { |dist| dist.except(field) }).for(:package_json) }
+ end
+
+ it { is_expected.not_to allow_value({}).for(:package_json) }
+
+ it { is_expected.not_to allow_value(test: 'test' * 10000).for(:package_json) }
+
+ def with_dist
+ valid_json.tap do |h|
+ h['dist'] = yield(h['dist'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 12a2a2a94a6..3fca139fc12 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to have_one(:debian_distribution).through(:debian_publication).source(:distribution).inverse_of(:packages).class_name('Packages::Debian::ProjectDistribution') }
it { is_expected.to have_one(:nuget_metadatum).inverse_of(:package) }
it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) }
+ it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) }
end
describe '.with_debian_codename' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4b3a6c731d5..109504ae1d8 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Project, factory_default: :keep do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
- it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id').inverse_of(:project) }
+ it { is_expected.to belong_to(:project_namespace).class_name('Namespaces::ProjectNamespace').with_foreign_key('project_namespace_id') }
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
@@ -191,7 +191,7 @@ RSpec.describe Project, factory_default: :keep do
# using delete rather than destroy due to `delete` skipping AR hooks/callbacks
# so it's ensured to work at the DB level. Uses AFTER DELETE trigger.
let_it_be(:project) { create(:project) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
it 'also deletes the associated ProjectNamespace' do
project.delete
@@ -233,6 +233,58 @@ RSpec.describe Project, factory_default: :keep do
expect(project.project_setting).to be_an_instance_of(ProjectSetting)
expect(project.project_setting).to be_new_record
end
+
+ context 'with project namespaces' do
+ it 'automatically creates a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path1')
+ project.save!
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
+ end
+
+ context 'with FF disabled' do
+ before do
+ stub_feature_flags(create_project_namespace_on_project_create: false)
+ end
+
+ it 'does not create a project namespace' do
+ project = build(:project, path: 'hopefully-valid-path2')
+ project.save!
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'updating a project' do
+ context 'with project namespaces' do
+ it 'keeps project namespace in sync with project' do
+ project = create(:project)
+ project.update!(path: 'hopefully-valid-path1')
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
+ end
+
+ context 'with FF disabled' do
+ before do
+ stub_feature_flags(create_project_namespace_on_project_create: false)
+ end
+
+ it 'does not create a project namespace when project is updated' do
+ project = create(:project)
+ project.update!(path: 'hopefully-valid-path1')
+
+ expect(project).to be_persisted
+ expect(project.project_namespace).to be_nil
+ end
+ end
+ end
end
context 'updating cd_cd_settings' do
@@ -322,6 +374,18 @@ RSpec.describe Project, factory_default: :keep do
create(:project)
end
+ context 'validates project namespace creation' do
+ it 'does not create project namespace if project is not created' do
+ project = build(:project, path: 'tree')
+
+ project.valid?
+
+ expect(project).not_to be_valid
+ expect(project).to be_new_record
+ expect(project.project_namespace).to be_new_record
+ end
+ end
+
context 'repository storages inclusion' do
let(:project2) { build(:project, repository_storage: 'missing') }
diff --git a/spec/policies/namespaces/project_namespace_policy_spec.rb b/spec/policies/namespaces/project_namespace_policy_spec.rb
index 22f3ccec1f8..5bb38deb498 100644
--- a/spec/policies/namespaces/project_namespace_policy_spec.rb
+++ b/spec/policies/namespaces/project_namespace_policy_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe NamespacePolicy do
let_it_be(:parent) { create(:namespace) }
- let_it_be(:namespace) { create(:project_namespace, parent: parent) }
+ let_it_be(:project) { create(:project, namespace: parent) }
+ let_it_be(:namespace) { project.project_namespace }
let(:permissions) do
[:owner_access, :create_projects, :admin_namespace, :read_namespace,
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
index 65f69d4056b..49046492ab4 100644
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ b/spec/presenters/packages/npm/package_presenter_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ::Packages::Npm::PackagePresenter do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:project) { create(:project) }
let_it_be(:package_name) { "@#{project.root_namespace.path}/test" }
let_it_be(:package1) { create(:npm_package, version: '2.0.4', project: project, name: package_name) }
@@ -13,42 +15,88 @@ RSpec.describe ::Packages::Npm::PackagePresenter do
let(:presenter) { described_class.new(package_name, packages) }
describe '#versions' do
- subject { presenter.versions }
+ let_it_be('package_json') do
+ {
+ 'name': package_name,
+ 'version': '2.0.4',
+ 'deprecated': 'warning!',
+ 'bin': './cli.js',
+ 'directories': ['lib'],
+ 'engines': { 'npm': '^7.5.6' },
+ '_hasShrinkwrap': false,
+ 'dist': {
+ 'tarball': 'http://localhost/tarball.tgz',
+ 'shasum': '1234567890'
+ },
+ 'custom_field': 'foo_bar'
+ }
+ end
- context 'for packages without dependencies' do
- it { is_expected.to be_a(Hash) }
- it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
+ let(:presenter) { described_class.new(package_name, packages, include_metadata: include_metadata) }
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- it { expect(subject.dig(package1.version, dependency_type)).to be nil }
- it { expect(subject.dig(package2.version, dependency_type)).to be nil }
- end
+ subject { presenter.versions }
- it 'avoids N+1 database queries' do
- check_n_plus_one(:versions) do
- create_list(:npm_package, 5, project: project, name: package_name)
+ where(:has_dependencies, :has_metadatum, :include_metadata) do
+ true | true | true
+ false | true | true
+ true | false | true
+ false | false | true
+
+ # TODO : to remove along with packages_npm_abbreviated_metadata
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/344827
+ true | true | false
+ false | true | false
+ true | false | false
+ false | false | false
+ end
+
+ with_them do
+ if params[:has_dependencies]
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) }
end
end
- end
- context 'for packages with dependencies' do
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- let_it_be("package_dependency_link_for_#{dependency_type}") { create(:packages_dependency_link, package: package1, dependency_type: dependency_type) }
+ if params[:has_metadatum]
+ let_it_be('package_metadatadum') { create(:npm_metadatum, package: package1, package_json: package_json) }
end
it { is_expected.to be_a(Hash) }
it { expect(subject[package1.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
it { expect(subject[package2.version].with_indifferent_access).to match_schema('public_api/v4/packages/npm_package_version') }
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any }
+ it { expect(subject[package1.version]['custom_field']).to be_blank }
+
+ context 'dependencies' do
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ if params[:has_dependencies]
+ it { expect(subject.dig(package1.version, dependency_type.to_s)).to be_any }
+ else
+ it { expect(subject.dig(package1.version, dependency_type)).to be nil }
+ end
+
+ it { expect(subject.dig(package2.version, dependency_type)).to be nil }
+ end
+ end
+
+ context 'metadatum' do
+ ::Packages::Npm::PackagePresenter::PACKAGE_JSON_ALLOWED_FIELDS.each do |metadata_field|
+ if params[:has_metadatum] && params[:include_metadata]
+ it { expect(subject.dig(package1.version, metadata_field)).not_to be nil }
+ else
+ it { expect(subject.dig(package1.version, metadata_field)).to be nil }
+ end
+
+ it { expect(subject.dig(package2.version, metadata_field)).to be nil }
+ end
end
it 'avoids N+1 database queries' do
check_n_plus_one(:versions) do
create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package|
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type)
+ if has_dependencies
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type)
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
index 2143abd3031..bea2365eaa6 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_draft_spec.rb
@@ -8,14 +8,14 @@ RSpec.describe 'Setting Draft status of a merge request' do
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
- let(:input) { { wip: true } }
+ let(:input) { { draft: true } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
- graphql_mutation(:merge_request_set_wip, variables.merge(input),
+ graphql_mutation(:merge_request_set_draft, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
@@ -28,7 +28,7 @@ RSpec.describe 'Setting Draft status of a merge request' do
end
def mutation_response
- graphql_mutation_response(:merge_request_set_wip)
+ graphql_mutation_response(:merge_request_set_draft)
end
before do
@@ -58,7 +58,7 @@ RSpec.describe 'Setting Draft status of a merge request' do
end
context 'when passing Draft false as input' do
- let(:input) { { wip: false } }
+ let(:input) { { draft: false } }
it 'does not do anything if the merge reqeust was not marked draft' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 43bbc6f3ca8..01dbf523071 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe API::Namespaces do
let_it_be(:group1) { create(:group, name: 'group.one') }
let_it_be(:group2) { create(:group, :nested) }
let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
describe "GET /namespaces" do
context "when unauthenticated" do
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 0d04c2cad5b..7c3f1890095 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -180,6 +180,7 @@ RSpec.describe API::NpmProjectPackages do
.to change { project.packages.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
.and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -317,6 +318,25 @@ RSpec.describe API::NpmProjectPackages do
end
end
end
+
+ context 'with a too large metadata structure' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:params) do
+ upload_params(package_name: package_name, package_version: '1.2.3').tap do |h|
+ h['versions']['1.2.3']['test'] = 'test' * 10000
+ end
+ end
+
+ it_behaves_like 'not a package tracking event'
+
+ it 'returns an error' do
+ expect { upload_package_with_token }
+ .not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include('Validation failed: Package json structure is too large')
+ end
+ end
end
def upload_package(package_name, params = {})
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 0c9e125cc90..097d374640c 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport do
it 'executes a limited number of queries' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
- expect(control_count).to be <= 100
+ expect(control_count).to be <= 101
end
it 'schedules an import using a namespace' do
diff --git a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
index 20b0546effa..5f7afdf699a 100644
--- a/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
+++ b/spec/services/dependency_proxy/find_or_create_blob_service_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe DependencyProxy::FindOrCreateBlobService do
let(:blob_sha) { blob.file_name.sub('.gz', '') }
it 'uses cached blob instead of downloading one' do
- expect { subject }.to change { blob.reload.updated_at }
+ expect { subject }.to change { blob.reload.read_at }
expect(subject[:status]).to eq(:success)
expect(subject[:blob]).to be_a(DependencyProxy::Blob)
diff --git a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
index 407ee42d345..ef608c9b113 100644
--- a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
shared_examples 'using the cached manifest' do
it 'uses cached manifest instead of downloading one', :aggregate_failures do
- subject
+ expect { subject }.to change { dependency_proxy_manifest.reload.read_at }
expect(subject[:status]).to eq(:success)
expect(subject[:manifest]).to be_a(DependencyProxy::Manifest)
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 21aa213a8ff..6712dccd249 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -185,9 +185,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
context 'when projects have project namespaces' do
let_it_be(:project1) { create(:project, :private, namespace: group) }
- let_it_be(:project_namespace1) { create(:project_namespace, project: project1) }
let_it_be(:project2) { create(:project, :private, namespace: group) }
- let_it_be(:project_namespace2) { create(:project_namespace, project: project2) }
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{group.path}" }
@@ -250,33 +248,42 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let_it_be(:membership) { create(:group_member, :owner, group: new_parent_group, user: user) }
let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) }
- before do
- group.update_attribute(:path, 'foo')
- end
-
- it 'returns false' do
- expect(transfer_service.execute(new_parent_group)).to be_falsy
- end
-
it 'adds an error on group' do
- transfer_service.execute(new_parent_group)
- expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ expect(transfer_service.error).to eq('Transfer failed: The parent group already has a subgroup or a project with the same path.')
end
- context 'when projects have project namespaces' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
+ # currently when a project is created it gets a corresponding project namespace
+ # so we test the case where a project without a project namespace is transferred
+ # for backward compatibility
+ context 'without project namespace' do
before do
- transfer_service.execute(new_parent_group)
+ project_namespace = project.project_namespace
+ project.update_column(:project_namespace_id, nil)
+ project_namespace.delete
end
- it_behaves_like 'project namespace path is in sync with project path' do
- let(:group_full_path) { "#{new_parent_group.full_path}" }
- let(:projects_with_project_namespace) { [project] }
+ it 'adds an error on group' do
+ expect(project.reload.project_namespace).to be_nil
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
end
end
end
+ context 'when projects have project namespaces' do
+ let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) }
+
+ before do
+ transfer_service.execute(new_parent_group)
+ end
+
+ it_behaves_like 'project namespace path is in sync with project path' do
+ let(:group_full_path) { "#{new_parent_group.full_path}" }
+ let(:projects_with_project_namespace) { [project] }
+ end
+ end
+
context 'when the group is allowed to be transferred' do
let_it_be(:new_parent_group, reload: true) { create(:group, :public) }
let_it_be(:new_parent_group_integration) { create(:integrations_slack, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') }
@@ -445,8 +452,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
context 'when transferring a group with project descendants' do
let!(:project1) { create(:project, :repository, :private, namespace: group) }
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
- let!(:project_namespace1) { create(:project_namespace, project: project1) }
- let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
@@ -483,8 +488,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let!(:project1) { create(:project, :repository, :public, namespace: group) }
let!(:project2) { create(:project, :repository, :public, namespace: group) }
let!(:new_parent_group) { create(:group, :private) }
- let!(:project_namespace1) { create(:project_namespace, project: project1) }
- let!(:project_namespace2) { create(:project_namespace, project: project2) }
it 'updates projects visibility to match the new parent' do
group.projects.each do |project|
@@ -504,8 +507,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
let!(:subgroup1) { create(:group, :private, parent: group) }
let!(:subgroup2) { create(:group, :internal, parent: group) }
- let!(:project_namespace1) { create(:project_namespace, project: project1) }
- let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index ba5729eaf59..9598355a640 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
let(:override) { {} }
let(:package_name) { "@#{namespace.path}/my-app" }
+ let(:version_data) { params.dig('versions', '1.0.1') }
subject { described_class.new(project, user, params).execute }
@@ -25,6 +26,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
.to change { Packages::Package.count }.by(1)
.and change { Packages::Package.npm.count }.by(1)
.and change { Packages::Tag.count }.by(1)
+ .and change { Packages::Npm::Metadatum.count }.by(1)
end
it_behaves_like 'assigns the package creator' do
@@ -40,6 +42,8 @@ RSpec.describe Packages::Npm::CreatePackageService do
expect(package.version).to eq(version)
end
+ it { expect(subject.npm_metadatum.package_json).to eq(version_data) }
+
it { expect(subject.name).to eq(package_name) }
it { expect(subject.version).to eq(version) }
@@ -54,6 +58,31 @@ RSpec.describe Packages::Npm::CreatePackageService do
expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
end
end
+
+ context 'with a too large metadata structure' do
+ before do
+ params[:versions][version][:test] = 'test' * 10000
+ end
+
+ it 'does not create the package' do
+ expect { subject }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package json structure is too large')
+ .and not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
+ end
+
+ context 'with packages_npm_abbreviated_metadata disabled' do
+ before do
+ stub_feature_flags(packages_npm_abbreviated_metadata: false)
+ end
+
+ it 'creates a package without metadatum' do
+ expect { subject }
+ .not_to change { Packages::Npm::Metadatum.count }
+ end
+ end
end
describe '#execute' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index bce65d19aae..d903523840c 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -49,6 +49,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'keeps them as specified' do
expect(project.name).to eq('one')
expect(project.path).to eq('two')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -58,6 +59,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'sets name == path' do
expect(project.path).to eq('one.two_three-four')
expect(project.name).to eq(project.path)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -67,6 +69,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'sets path == name' do
expect(project.name).to eq('one.two_three-four')
expect(project.path).to eq(project.name)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -78,6 +81,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'parameterizes the name' do
expect(project.name).to eq('one.two_three-four and five')
expect(project.path).to eq('one-two_three-four-and-five')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -111,13 +115,14 @@ RSpec.describe Projects::CreateService, '#execute' do
end
context 'user namespace' do
- it do
+ it 'creates a project in user namespace' do
project = create_project(user, opts)
expect(project).to be_valid
expect(project.owner).to eq(user)
expect(project.team.maintainers).to include(user)
expect(project.namespace).to eq(user.namespace)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -151,6 +156,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.owner).to eq(user)
expect(project.team.maintainers).to contain_exactly(user)
expect(project.namespace).to eq(user.namespace)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -160,6 +166,7 @@ RSpec.describe Projects::CreateService, '#execute' do
project = create_project(admin, opts)
expect(project).not_to be_persisted
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -183,6 +190,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.namespace).to eq(group)
expect(project.team.owners).to include(user)
expect(user.authorized_projects).to include(project)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -339,6 +347,7 @@ RSpec.describe Projects::CreateService, '#execute' do
end
imported_project
+ expect(imported_project.project_namespace).to be_in_sync_with_project(imported_project)
end
it 'stores import data and URL' do
@@ -406,6 +415,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.visibility_level).to eq(project_level)
expect(project).to be_saved
expect(project).to be_valid
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -424,6 +434,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.errors.messages[:visibility_level].first).to(
match('restricted by your GitLab administrator')
)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
it 'does not allow a restricted visibility level for admins when admin mode is disabled' do
@@ -493,6 +504,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.owner).to eq(user)
expect(project.namespace).to eq(user.namespace)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
context 'when another repository already exists on disk' do
@@ -522,6 +534,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to respond_to(:errors)
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
it 'does not allow to import project when path matches existing repository on disk' do
@@ -531,6 +544,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to respond_to(:errors)
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
@@ -555,6 +569,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to respond_to(:errors)
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -870,6 +885,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.shared_runners_enabled).to eq(expected_result_for_project)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -890,6 +906,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.shared_runners_enabled).to eq(expected_result_for_project)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -907,6 +924,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project.persisted?).to eq(false)
expect(project).to be_invalid
expect(project.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group does not allow it')
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
@@ -926,6 +944,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.shared_runners_enabled).to eq(expected_result)
+ expect(project.project_namespace).to be_in_sync_with_project(project)
end
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index b539b01066e..2164580a158 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -66,8 +66,6 @@ RSpec.describe Projects::TransferService do
end
context 'when project has an associated project namespace' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
@@ -272,8 +270,6 @@ RSpec.describe Projects::TransferService do
end
context 'when project has an associated project namespace' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
it 'keeps project namespace in sync with project' do
attempt_project_transfer
@@ -294,8 +290,6 @@ RSpec.describe Projects::TransferService do
end
context 'when project has an associated project namespace' do
- let!(:project_namespace) { create(:project_namespace, project: project) }
-
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6a0d9ea0669..47aea2c1c37 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -197,6 +197,14 @@ RSpec.configure do |config|
if ENV['CI'] || ENV['RETRIES']
# This includes the first try, i.e. tests will be run 4 times before failing.
config.default_retry_count = ENV.fetch('RETRIES', 3).to_i + 1
+
+ # Do not retry controller tests because rspec-retry cannot properly
+ # reset the controller which may contain data from last attempt. See
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73360
+ config.around(:each, type: :controller) do |example|
+ example.run_with_retry(retry: 1)
+ end
+
config.exceptions_to_hard_fail = [DeprecationToolkitEnv::DeprecationBehaviors::SelectiveRaise::RaiseDisallowedDeprecation]
end
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index 813c6176317..81e669aab57 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -4,6 +4,7 @@ module GpgHelpers
SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4'
SIGNED_AND_AUTHORED_SHA = '3c1d9a0266cb0c62d926f4a6c649beed561846f5'
DIFFERING_EMAIL_SHA = 'a17a9f66543673edf0a3d1c6b93bdda3fe600f32'
+ MULTIPLE_SIGNATURES_SHA = 'c7794c14268d67ad8a2d5f066d706539afc75a96'
module User1
extend self
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 3b89820be07..ae5687c1e2d 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -9,7 +9,7 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
- 'signed-commits' => '6101e87',
+ 'signed-commits' => 'c7794c1',
'not-merged-branch' => 'b83d6e3',
'branch-merged' => '498214d',
'empty-branch' => '7efb185',
diff --git a/spec/support/matchers/project_namespace_matcher.rb b/spec/support/matchers/project_namespace_matcher.rb
new file mode 100644
index 00000000000..95aa5429679
--- /dev/null
+++ b/spec/support/matchers/project_namespace_matcher.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :be_in_sync_with_project do |project|
+ match do |project_namespace|
+ # if project is not persisted make sure we do not have a persisted project_namespace for it
+ break false if project.new_record? && project_namespace&.persisted?
+ # don't really care if project is not in sync if the project was never persisted.
+ break true if project.new_record? && !project_namespace.present?
+
+ project_namespace.present? &&
+ project.name == project_namespace.name &&
+ project.path == project_namespace.path &&
+ project.namespace == project_namespace.parent &&
+ project.visibility_level == project_namespace.visibility_level &&
+ project.shared_runners_enabled == project_namespace.shared_runners_enabled
+ end
+
+ failure_message_when_negated do |project_namespace|
+ if project.new_record? && project_namespace&.persisted?
+ "expected that a non persisted project #{project} does not have a persisted project namespace #{project_namespace}"
+ else
+ <<-MSG
+ expected that the project's attributes name, path, namespace_id, visibility_level, shared_runners_enabled
+ are in sync with the corresponding project namespace attributes
+ MSG
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
index a4e0d6c871e..2d08de297a3 100644
--- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
@@ -11,18 +11,18 @@ RSpec.shared_examples 'ttl_expirable' do
it { is_expected.to validate_presence_of(:status) }
end
- describe '.updated_before' do
+ describe '.read_before' do
# rubocop:disable Rails/SaveBang
let_it_be_with_reload(:item1) { create(class_symbol) }
let_it_be(:item2) { create(class_symbol) }
# rubocop:enable Rails/SaveBang
before do
- item1.update_column(:updated_at, 1.month.ago)
+ item1.update_column(:read_at, 1.month.ago)
end
it 'returns items with created at older than the supplied number of days' do
- expect(described_class.updated_before(10)).to contain_exactly(item1)
+ expect(described_class.read_before(10)).to contain_exactly(item1)
end
end
@@ -48,4 +48,13 @@ RSpec.shared_examples 'ttl_expirable' do
expect(described_class.lock_next_by(:created_at)).to contain_exactly(item3)
end
end
+
+ describe '#read', :freeze_time do
+ let_it_be(:old_read_at) { 1.day.ago }
+ let_it_be(:item1) { create(class_symbol, read_at: old_read_at) }
+
+ it 'updates read_at' do
+ expect { item1.read! }.to change { item1.reload.read_at }
+ end
+ end
end
diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb
index 0e60476f10a..ac6a843663f 100644
--- a/spec/support/shared_examples/namespaces/traversal_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_examples.rb
@@ -23,7 +23,7 @@ RSpec.shared_examples 'namespace traversal' do
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
let_it_be(:project) { create(:project, group: nested_group) }
- let_it_be(:project_namespace) { create(:project_namespace, project: project) }
+ let_it_be(:project_namespace) { project.project_namespace }
describe '#root_ancestor' do
it 'returns the correct root ancestor' do
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index 2af7b616659..19677e92001 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -8,6 +8,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
+ let_it_be(:package_metadatum) { create(:npm_metadatum, package: package) }
+
let(:headers) { {} }
subject { get(url, headers: headers) }
@@ -39,6 +41,19 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
# query count can slightly change between the examples so we're using a custom threshold
expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4)
end
+
+ context 'with packages_npm_abbreviated_metadata disabled' do
+ before do
+ stub_feature_flags(packages_npm_abbreviated_metadata: false)
+ end
+
+ it 'calls the presenter without including metadata' do
+ expect(::Packages::Npm::PackagePresenter)
+ .to receive(:new).with(anything, anything, include_metadata: false).and_call_original
+
+ subject
+ end
+ end
end
shared_examples 'reject metadata request' do |status:|
diff --git a/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb b/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb
index dcc36c93327..0a4d283a983 100644
--- a/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb
+++ b/spec/views/jira_connect/subscriptions/index.html.haml_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'jira_connect/subscriptions/index.html.haml' do
before do
allow(view).to receive(:current_user).and_return(user)
- assign(:subscriptions, [])
+ assign(:subscriptions, create_list(:jira_connect_subscription, 1))
end
context 'when the user is signed in' do
diff --git a/spec/views/layouts/_published_experiments.html.haml_spec.rb b/spec/views/layouts/_published_experiments.html.haml_spec.rb
new file mode 100644
index 00000000000..d1ade8ddd6e
--- /dev/null
+++ b/spec/views/layouts/_published_experiments.html.haml_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'layouts/_published_experiments', :experiment do
+ before do
+ stub_const('TestControlExperiment', ApplicationExperiment)
+ stub_const('TestCandidateExperiment', ApplicationExperiment)
+ stub_const('TestExcludedExperiment', ApplicationExperiment)
+
+ TestControlExperiment.new('test_control').tap do |e|
+ e.variant(:control)
+ e.publish
+ end
+ TestCandidateExperiment.new('test_candidate').tap do |e|
+ e.variant(:candidate)
+ e.publish
+ end
+ TestExcludedExperiment.new('test_excluded').tap do |e|
+ e.exclude!
+ e.publish
+ end
+
+ render
+ end
+
+ it 'renders out data for all non-excluded, published experiments' do
+ output = rendered
+
+ expect(output).to include('gl.experiments = {')
+ expect(output).to match(/"test_control":\{[^}]*"variant":"control"/)
+ expect(output).to match(/"test_candidate":\{[^}]*"variant":"candidate"/)
+ expect(output).not_to include('"test_excluded"')
+ end
+end
diff --git a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
index d3234f4c212..ae0cb097ebf 100644
--- a/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
+++ b/spec/workers/dependency_proxy/image_ttl_group_policy_worker_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe DependencyProxy::ImageTtlGroupPolicyWorker do
subject { worker.perform }
context 'when there are images to expire' do
- let_it_be_with_reload(:old_blob) { create(:dependency_proxy_blob, group: group, updated_at: 1.year.ago) }
- let_it_be_with_reload(:old_manifest) { create(:dependency_proxy_manifest, group: group, updated_at: 1.year.ago) }
+ let_it_be_with_reload(:old_blob) { create(:dependency_proxy_blob, group: group, read_at: 1.year.ago) }
+ let_it_be_with_reload(:old_manifest) { create(:dependency_proxy_manifest, group: group, read_at: 1.year.ago) }
let_it_be_with_reload(:new_blob) { create(:dependency_proxy_blob, group: group) }
let_it_be_with_reload(:new_manifest) { create(:dependency_proxy_manifest, group: group) }