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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-23 15:09:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-23 15:09:33 +0300
commitb38fc20ae0e90d5b1c538a139aa0a7da1b7b5726 (patch)
tree3ce77cdb707b75c9d74c6ff2a8386dd06bd48b44
parentb3647b2a67930e8aa3c1b1dd9bda29c368c862ba (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/issue_templates/OSS_Partner.md (renamed from .gitlab/issue_templates/Migrations.md)13
-rw-r--r--.rubocop_todo.yml6
-rw-r--r--app/assets/javascripts/captcha/apollo_captcha_link.js37
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue6
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue16
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/remove_member_modal.vue6
-rw-r--r--app/assets/stylesheets/fontawesome_custom.scss43
-rw-r--r--app/assets/stylesheets/framework/common.scss5
-rw-r--r--app/assets/stylesheets/framework/filters.scss4
-rw-r--r--app/assets/stylesheets/framework/modal.scss8
-rw-r--r--app/assets/stylesheets/framework/page_header.scss6
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss78
-rw-r--r--app/controllers/concerns/membership_actions.rb9
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/mailers/emails/in_product_marketing.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/namespace.rb9
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb69
-rw-r--r--app/serializers/member_entity.rb2
-rw-r--r--app/services/boards/base_item_move_service.rb14
-rw-r--r--app/services/boards/issues/move_service.rb23
-rw-r--r--app/services/ci/process_build_service.rb25
-rw-r--r--app/services/issuable_base_service.rb14
-rw-r--r--app/services/issues/update_service.rb16
-rw-r--r--app/services/users/activity_service.rb2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml4
-rw-r--r--app/views/search/results/_empty.html.haml2
-rw-r--r--app/views/shared/_search_settings.html.haml7
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--changelogs/unreleased/26522-commit-message-link-line-break.yml5
-rw-r--r--changelogs/unreleased/294025-rollout-search-settings.yml5
-rw-r--r--changelogs/unreleased/297240-remove-skip_dag_manual_and_delayed_jobs.yml5
-rw-r--r--changelogs/unreleased/299685-fix-packages-build-info-when-pushed-with-job-token.yml5
-rw-r--r--changelogs/unreleased/31343-remove-unnecessary-use-of-freeze.yml5
-rw-r--r--changelogs/unreleased/31343-remove-unnecessary-use-of-freeze2.yml5
-rw-r--r--changelogs/unreleased/31343-remove-unnecessary-use-of-freeze3.yml5
-rw-r--r--changelogs/unreleased/31343-remove-unnecessary-use-of-freeze5.yml5
-rw-r--r--changelogs/unreleased/fix-option-remove-memberships-from-subresources.yml6
-rw-r--r--changelogs/unreleased/pl-rubocop-todo-department-name.yml5
-rw-r--r--changelogs/unreleased/revert-inner-join-cte-fix.yml5
-rw-r--r--changelogs/unreleased/sh-fix-encoded-api-project-urls.yml5
-rw-r--r--config/feature_flags/development/ci_external_validation_service.yml (renamed from config/feature_flags/development/skip_dag_manual_and_delayed_jobs.yml)12
-rw-r--r--config/feature_flags/development/ci_lower_frequency_trace_update.yml8
-rw-r--r--config/feature_flags/development/optimize_deploy_keys_presenter.yml (renamed from config/feature_flags/development/search_settings_in_page.yml)10
-rw-r--r--config/feature_flags/development/pipeline_graph_layers_view.yml8
-rw-r--r--config/feature_flags/development/recursive_namespace_lookup_as_inner_join.yml8
-rw-r--r--config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml8
-rw-r--r--config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml8
-rw-r--r--config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml8
-rw-r--r--config/feature_flags/ops/redis_hll_tracking.yml8
-rw-r--r--config/feature_flags/ops/x509_forced_cert_loading.yml8
-rw-r--r--doc/administration/external_pipeline_validation.md4
-rw-r--r--doc/administration/pages/index.md30
-rw-r--r--doc/api/members.md5
-rw-r--r--doc/development/fe_guide/graphql.md28
-rw-r--r--doc/user/search/index.md31
-rw-r--r--lib/api/helpers.rb6
-rw-r--r--lib/api/helpers/authentication.rb5
-rw-r--r--lib/api/internal/base.rb2
-rw-r--r--lib/api/members.rb4
-rw-r--r--lib/api/v3/github.rb2
-rw-r--r--lib/banzai/filter/commit_trailers_filter.rb2
-rw-r--r--lib/bulk_imports/clients/http.rb2
-rw-r--r--lib/csv_builder.rb2
-rw-r--r--lib/gitlab/auth/auth_finders.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/external.rb28
-rw-r--r--lib/gitlab/ci/trace.rb13
-rw-r--r--lib/gitlab/conan_token.rb2
-rw-r--r--lib/gitlab/health_checks/gitaly_check.rb2
-rw-r--r--lib/gitlab/http_connection_adapter.rb14
-rw-r--r--lib/gitlab/pages.rb2
-rw-r--r--lib/gitlab/usage_data.rb2
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb2
-rw-r--r--lib/learn_gitlab.rb6
-rw-r--r--locale/gitlab.pot20
-rw-r--r--rubocop/cop/migration/hash_index.rb2
-rw-r--r--rubocop/cop/migration/prevent_strings.rb2
-rw-r--r--rubocop/cop/migration/remove_column.rb2
-rw-r--r--rubocop/cop/migration/remove_concurrent_index.rb2
-rw-r--r--rubocop/cop/migration/remove_index.rb2
-rw-r--r--rubocop/cop/migration/safer_boolean_column.rb6
-rw-r--r--rubocop/cop/migration/timestamps.rb2
-rw-r--r--rubocop/cop/migration/update_column_in_batches.rb2
-rw-r--r--rubocop/cop/migration/with_lock_retries_with_change.rb2
-rw-r--r--rubocop/cop/usage_data/distinct_count_by_large_foreign_key.rb2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb16
-rw-r--r--spec/features/admin/admin_search_settings_spec.rb6
-rw-r--r--spec/features/boards/sidebar_assignee_spec.rb22
-rw-r--r--spec/features/boards/sidebar_due_date_spec.rb45
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb24
-rw-r--r--spec/features/boards/sidebar_milestones_spec.rb64
-rw-r--r--spec/features/boards/sidebar_spec.rb138
-rw-r--r--spec/features/boards/sidebar_subscription_spec.rb52
-rw-r--r--spec/features/boards/sidebar_time_tracking_spec.rb50
-rw-r--r--spec/features/file_uploads/maven_package_spec.rb21
-rw-r--r--spec/features/file_uploads/nuget_package_spec.rb4
-rw-r--r--spec/features/file_uploads/rubygem_package_spec.rb45
-rw-r--r--spec/features/groups/settings/user_searches_in_settings_spec.rb8
-rw-r--r--spec/features/profiles/user_search_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/user_searches_in_settings_spec.rb6
-rw-r--r--spec/fixtures/api/schemas/entities/member.json2
-rw-r--r--spec/frontend/__helpers__/mock_apollo_helper.js12
-rw-r--r--spec/frontend/captcha/apollo_captcha_link_spec.js165
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js1
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js37
-rw-r--r--spec/frontend/members/mock_data.js1
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/remove_member_modal_spec.js35
-rw-r--r--spec/lib/api/helpers/authentication_spec.rb15
-rw-r--r--spec/lib/banzai/filter/commit_trailers_filter_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb108
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb10
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb125
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb22
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb20
-rw-r--r--spec/presenters/projects/settings/deploy_keys_presenter_spec.rb57
-rw-r--r--spec/requests/api/ci/runner/jobs_trace_spec.rb17
-rw-r--r--spec/requests/api/members_spec.rb28
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb8
-rw-r--r--spec/requests/api/pypi_packages_spec.rb2
-rw-r--r--spec/services/ci/process_build_service_spec.rb23
-rw-r--r--spec/services/members/destroy_service_spec.rb157
-rw-r--r--spec/support/helpers/stub_env.rb2
-rw-r--r--spec/support/helpers/stub_requests.rb2
-rw-r--r--spec/support/helpers/test_env.rb2
-rw-r--r--spec/support/shared_examples/features/search_settings_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb13
-rw-r--r--spec/views/layouts/profile.html.haml_spec.rb19
-rw-r--r--tooling/danger/project_helper.rb2
-rw-r--r--tooling/lib/tooling/test_map_packer.rb2
-rw-r--r--workhorse/internal/api/api.go30
-rw-r--r--workhorse/internal/upstream/routes.go26
-rw-r--r--workhorse/upload_test.go84
140 files changed, 1784 insertions, 776 deletions
diff --git a/.gitlab/issue_templates/Migrations.md b/.gitlab/issue_templates/OSS_Partner.md
index 822722a0f71..d9c05026e7c 100644
--- a/.gitlab/issue_templates/Migrations.md
+++ b/.gitlab/issue_templates/OSS_Partner.md
@@ -1,10 +1,9 @@
-# Project Name | Migration Tracker
-<!-- Please edit this header with your project / organization's name. -->
+<!-- Please title your issue with the following format: "Project Name | Issue Tracker". -->
## Background
<!--
-Please add information here about why you're planning on migrating. Include any initial announcements that have been made about the decision or status.
+Please add information here about why your project is considering a migration to GitLab, or why it decided to do so. Include any initial announcements that have been / were made about the decision or status.
-->
### Goals
@@ -16,7 +15,7 @@ Please add information here about why you're planning on migrating. Include any
<!-- Please complete as many items in this list as possible. If you're not sure yet, add "TBD" (To be Decided) or "Unknown" -->
* **Timeline.** -
- * **Product.** - GitLab Gold/Ultimate or Community Edition
+ * **Product.** - SaaS-Ultimate/Self-Managed-Ultimate or Community Edition
* **Project's License.** What kind of OSI-approved license does your project use?
## Current Tooling and Replacements
@@ -31,6 +30,8 @@ Please fill in the table to give an overview of your current tooling. Here's a d
Here's an example of a replacements overview from one of the projects which migrated to GitLab: https://gitlab.com/gitlab-org/gitlab/-/issues/25657#gitlab-replacements
+Consider deleting the table below if you are unable to expand upon your current tooling.
+
-->
| Tool | Feature | GitLab feature | GitLab edition |
@@ -63,5 +64,5 @@ Here is an example of what this list might look like once populated: https://git
------
-/label ~"Open Source" ~movingtogitlab
-/cc @nuritzi
+/label ~"Open Source Partners"
+/cc @nuritzi @greg
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index b217c939c7a..35778f056b4 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -208,12 +208,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity:
Max: 25
-# Offense count: 1
-# Cop supports --auto-correct.
-Migration/DepartmentName:
- Exclude:
- - 'app/models/commit.rb'
-
# Offense count: 196
# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, Regex, IgnoreExecutableScripts, AllowedAcronyms.
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
diff --git a/app/assets/javascripts/captcha/apollo_captcha_link.js b/app/assets/javascripts/captcha/apollo_captcha_link.js
new file mode 100644
index 00000000000..e49abc10b29
--- /dev/null
+++ b/app/assets/javascripts/captcha/apollo_captcha_link.js
@@ -0,0 +1,37 @@
+import { ApolloLink, Observable } from 'apollo-link';
+
+export const apolloCaptchaLink = new ApolloLink((operation, forward) =>
+ forward(operation).flatMap((result) => {
+ const { errors = [] } = result;
+
+ // Our API will return with a top-level GraphQL error with extensions
+ // in case a captcha is required.
+ const captchaError = errors.find((e) => e?.extensions?.needs_captcha_response);
+ if (captchaError) {
+ const captchaSiteKey = captchaError.extensions.captcha_site_key;
+ const spamLogId = captchaError.extensions.spam_log_id;
+
+ return new Observable((observer) => {
+ import('~/captcha/wait_for_captcha_to_be_solved')
+ .then(({ waitForCaptchaToBeSolved }) => waitForCaptchaToBeSolved(captchaSiteKey))
+ .then((captchaResponse) => {
+ // If the captcha was solved correctly, we re-do our action while setting
+ // captcha response headers.
+ operation.setContext({
+ headers: {
+ 'X-GitLab-Captcha-Response': captchaResponse,
+ 'X-GitLab-Spam-Log-Id': spamLogId,
+ },
+ });
+ forward(operation).subscribe(observer);
+ })
+ .catch((error) => {
+ observer.error(error);
+ observer.complete();
+ });
+ });
+ }
+
+ return Observable.of(result);
+ }),
+);
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index cb71be39ebc..3777f1dbb97 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -13,6 +13,11 @@ export default {
type: Number,
required: true,
},
+ memberType: {
+ type: String,
+ required: false,
+ default: null,
+ },
message: {
type: String,
required: true,
@@ -50,6 +55,7 @@ export default {
:aria-label="title"
:icon="icon"
:data-member-path="computedMemberPath"
+ :data-member-type="memberType"
:data-is-access-request="isAccessRequest"
:data-message="message"
data-qa-selector="delete_member_button"
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index f779d1755a5..e723685d88b 100644
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -59,6 +59,7 @@ export default {
<remove-member-button
v-else
:member-id="member.id"
+ :member-type="member.type"
:message="message"
:title="s__('Member|Remove member')"
/>
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index caa269f5095..e44458f8f84 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -10,3 +10,6 @@ export const ONE_COL_WIDTH = 180;
export const REST = 'rest';
export const GRAPHQL = 'graphql';
+
+export const STAGE_VIEW = 'stage';
+export const LAYER_VIEW = 'layer';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 962f2ca2a4c..d5aa6f42c51 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -2,8 +2,11 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
+import { STAGE_VIEW } from './constants';
import PipelineGraph from './graph_component.vue';
+import GraphViewSelector from './graph_view_selector.vue';
import {
getQueryHeaders,
reportToSentry,
@@ -17,8 +20,10 @@ export default {
components: {
GlAlert,
GlLoadingIcon,
+ GraphViewSelector,
PipelineGraph,
},
+ mixins: [glFeatureFlagMixin()],
inject: {
graphqlResourceEtag: {
default: '',
@@ -35,8 +40,9 @@ export default {
},
data() {
return {
- pipeline: null,
alertType: null,
+ currentViewType: STAGE_VIEW,
+ pipeline: null,
showAlert: false,
};
},
@@ -147,6 +153,9 @@ export default {
}
},
/* eslint-enable @gitlab/require-i18n-strings */
+ updateViewType(type) {
+ this.currentViewType = type;
+ },
},
};
</script>
@@ -155,6 +164,11 @@ export default {
<gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
{{ alert.text }}
</gl-alert>
+ <graph-view-selector
+ v-if="glFeatures.pipelineGraphLayersView"
+ :type="currentViewType"
+ @updateViewType="updateViewType"
+ />
<gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
<pipeline-graph
v-if="pipeline"
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
new file mode 100644
index 00000000000..080a9831574
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { STAGE_VIEW, LAYER_VIEW } from './constants';
+
+export default {
+ name: 'GraphViewSelector',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlSprintf,
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentViewType: STAGE_VIEW,
+ };
+ },
+ i18n: {
+ labelText: __('Order jobs by'),
+ },
+ views: {
+ [STAGE_VIEW]: {
+ type: STAGE_VIEW,
+ text: {
+ primary: __('Stage'),
+ secondary: __('View the jobs grouped into stages'),
+ },
+ },
+ [LAYER_VIEW]: {
+ type: LAYER_VIEW,
+ text: {
+ primary: __('%{codeStart}needs:%{codeEnd} relationships'),
+ secondary: __('View what jobs are needed for a job to run'),
+ },
+ },
+ },
+ computed: {
+ currentDropdownText() {
+ return this.$options.views[this.type].text.primary;
+ },
+ },
+ methods: {
+ itemClick(type) {
+ this.$emit('updateViewType', type);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-end gl-align-items-center gl-my-4">
+ <span>{{ $options.i18n.labelText }}</span>
+ <gl-dropdown class="gl-ml-4" :right="true">
+ <template #button-content>
+ <gl-sprintf :message="currentDropdownText">
+ <template #code="{ content }">
+ <code> {{ content }} </code>
+ </template>
+ </gl-sprintf>
+ <gl-icon class="gl-px-2" name="angle-down" :size="18" />
+ </template>
+ <gl-dropdown-item
+ v-for="view in $options.views"
+ :key="view.type"
+ :secondary-text="view.text.secondary"
+ @click="itemClick(view.type)"
+ >
+ <b>
+ <gl-sprintf :message="view.text.primary">
+ <template #code="{ content }">
+ <code> {{ content }} </code>
+ </template>
+ </gl-sprintf>
+ </b>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
index 88d1b15aee3..2a19537f820 100644
--- a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
@@ -22,6 +22,9 @@ export default {
isAccessRequest() {
return parseBoolean(this.modalData.isAccessRequest);
},
+ isGroupMember() {
+ return this.modalData.memberType === 'GroupMember';
+ },
actionText() {
return this.isAccessRequest ? __('Deny access request') : __('Remove member');
},
@@ -70,6 +73,9 @@ export default {
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">
+ {{ __('Also remove direct user membership from subgroups and projects') }}
+ </gl-form-checkbox>
<gl-form-checkbox v-if="!isAccessRequest" name="unassign_issuables">
{{ __('Also unassign this user from related issues and merge requests') }}
</gl-form-checkbox>
diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss
deleted file mode 100644
index b9bb3edaaab..00000000000
--- a/app/assets/stylesheets/fontawesome_custom.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-// Custom Font Awesome styles that render emojis in asciidoc
-.md {
- .fa {
- display: inline-block;
- font-style: normal;
- font-size: 14px;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
-
- .fa-2x {
- font-size: 2em;
- }
-
- .fa-exclamation-triangle::before {
- content: '⚠';
- }
-
- .fa-exclamation-circle::before {
- content: '❗';
- }
-
- .fa-lightbulb-o::before {
- content: '💡';
- }
-
- .fa-thumb-tack::before {
- content: '📌';
- }
-
- .fa-fire::before {
- content: '🔥';
- }
-
- .fa-square-o::before {
- content: '\2610';
- }
-
- .fa-check-square-o::before {
- content: '\2611';
- }
-}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 5d182373fb1..54a2284a3ab 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -292,11 +292,6 @@ img.emoji {
}
}
-.search_box {
- @extend .card.card-body;
- text-align: center;
-}
-
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index a4af45a467c..c835b1d6e26 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -204,10 +204,6 @@
margin-bottom: 10px;
}
- &:hover {
- @extend .form-control:hover;
- }
-
&.focus,
&.focus:hover {
border-color: $blue-300;
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index ec433434573..48a18e0d145 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -94,14 +94,6 @@ body.modal-open {
padding-right: 0 !important;
}
-.modal-no-backdrop {
- @extend .modal-dialog;
-
- .modal-content {
- box-shadow: none;
- }
-}
-
.modal {
background-color: $black-transparent;
diff --git a/app/assets/stylesheets/framework/page_header.scss b/app/assets/stylesheets/framework/page_header.scss
index 660e3dcac8d..c0847382544 100644
--- a/app/assets/stylesheets/framework/page_header.scss
+++ b/app/assets/stylesheets/framework/page_header.scss
@@ -32,8 +32,10 @@
}
.avatar {
- @extend .avatar-inline;
- margin-left: 0;
+ float: none;
+ display: inline-block;
+ margin-left: 2px;
+ flex-shrink: 0;
@include media-breakpoint-up(sm) {
margin-left: 4px;
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 07c3eb19fd4..f57d906e73c 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -3,6 +3,7 @@
max-width: #{$max + '%'};
}
+.gl-responsive-table-row,
.gl-responsive-table-row-layout {
width: 100%;
@@ -17,7 +18,6 @@
}
.gl-responsive-table-row {
- @extend .gl-responsive-table-row-layout;
margin-top: 10px;
border: 1px solid $border-color;
color: $gray-500;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 5624a6ea8a3..7685a173e57 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -1,6 +1,3 @@
-// Custom Fontawesome icons
-@import 'fontawesome_custom';
-
/**
* Apply Markup (Markdown/AsciiDoc) typography
*
@@ -435,7 +432,9 @@
}
}
- a.with-attachment-icon {
+ a.with-attachment-icon,
+ a[href*='/uploads/'],
+ a[href*='storage.googleapis.com/google-code-attachments/'] {
&::before {
margin-right: 4px;
@@ -449,8 +448,6 @@
a[href*='/uploads/'],
a[href*='storage.googleapis.com/google-code-attachments/'] {
- @extend .with-attachment-icon;
-
&.no-attachment-icon {
&::before {
display: none;
@@ -507,32 +504,56 @@
text-decoration: line-through;
}
- .admonitionblock td.icon {
- width: 1%;
+ // Custom Font Awesome styles that render emojis in asciidoc
+ .fa {
+ display: inline-block;
+ font-style: normal;
+ font-size: 14px;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
- [class^='fa icon-'] {
- @extend .fa-2x;
- }
+ .fa-2x,
+ .admonitionblock td.icon [class^='fa icon-'] {
+ font-size: 2em;
+ }
- .icon-note {
- @extend .fa-thumb-tack;
- }
+ .fa-exclamation-triangle::before,
+ .admonitionblock td.icon .icon-warning::before {
+ content: '⚠';
+ }
- .icon-tip {
- @extend .fa-lightbulb-o;
- }
+ .fa-exclamation-circle::before,
+ .admonitionblock td.icon .icon-important::before {
+ content: '❗';
+ }
- .icon-warning {
- @extend .fa-exclamation-triangle;
- }
+ .fa-lightbulb-o::before,
+ .admonitionblock td.icon .icon-tip::before {
+ content: '💡';
+ }
- .icon-caution {
- @extend .fa-fire;
- }
+ .fa-thumb-tack::before,
+ .admonitionblock td.icon .icon-note::before {
+ content: '📌';
+ }
- .icon-important {
- @extend .fa-exclamation-circle;
- }
+ .fa-fire::before,
+ .admonitionblock td.icon .icon-caution::before {
+ content: '🔥';
+ }
+
+ .fa-square-o::before {
+ content: '\2610';
+ }
+
+ .fa-check-square-o::before {
+ content: '\2611';
+ }
+
+ .admonitionblock td.icon {
+ width: 1%;
}
.metrics-embed {
@@ -640,12 +661,13 @@ code {
.commit-sha,
.ref-name,
.pipeline-number {
- @extend .monospace;
+ font-family: $monospace-font;
font-size: 95%;
}
.git-revision-dropdown .dropdown-content ul li a {
- @extend .ref-name;
+ font-family: $monospace-font;
+ font-size: 95%;
word-break: break-all;
}
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 9e3625d1b36..54f1fbc9fd2 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -43,10 +43,11 @@ module MembershipActions
def destroy
member = membershipable.members_and_requesters.find(params[:id])
+ skip_subresources = !ActiveRecord::Type::Boolean.new.cast(params.delete(:remove_sub_memberships))
# !! is used in case unassign_issuables contains empty string which would result in nil
unassign_issuables = !!ActiveRecord::Type::Boolean.new.cast(params.delete(:unassign_issuables))
- Members::DestroyService.new(current_user).execute(member, unassign_issuables: unassign_issuables)
+ Members::DestroyService.new(current_user).execute(member, skip_subresources: skip_subresources, unassign_issuables: unassign_issuables)
respond_to do |format|
format.html do
@@ -54,7 +55,11 @@ module MembershipActions
begin
case membershipable
when Namespace
- _("User was successfully removed from group and any subresources.")
+ if skip_subresources
+ _("User was successfully removed from group.")
+ else
+ _("User was successfully removed from group and any subgroups and projects.")
+ end
else
_("User was successfully removed from project.")
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 6f2cb0ca669..35d138fc27b 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -14,6 +14,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:pipeline_graph_layers_view, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:jira_for_vulnerabilities, project, type: :development, default_enabled: :yaml)
diff --git a/app/mailers/emails/in_product_marketing.rb b/app/mailers/emails/in_product_marketing.rb
index 0be9ec5f915..c55e178b70d 100644
--- a/app/mailers/emails/in_product_marketing.rb
+++ b/app/mailers/emails/in_product_marketing.rb
@@ -4,7 +4,7 @@ module Emails
module InProductMarketing
include InProductMarketingHelper
- FROM_ADDRESS = 'GitLab <team@gitlab.com>'.freeze
+ FROM_ADDRESS = 'GitLab <team@gitlab.com>'
CUSTOM_HEADERS = {
'X-Mailgun-Track' => 'yes',
'X-Mailgun-Track-Clicks' => 'yes',
diff --git a/app/models/commit.rb b/app/models/commit.rb
index bf168aaacc5..5b1f236b333 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -62,7 +62,7 @@ class Commit
collection.sort do |a, b|
operands = [a, b].tap { |o| o.reverse! if sort == 'desc' }
- attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend
+ attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable GitlabSecurity/PublicSend
# use case insensitive comparison for string values
order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 6f6c8b387a1..e6809316274 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -261,13 +261,8 @@ class Namespace < ApplicationRecord
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
- return Project.where(namespace: self) if user?
-
- if Feature.enabled?(:recursive_namespace_lookup_as_inner_join, self)
- Project.joins("INNER JOIN (#{self_and_descendants.select(:id).to_sql}) namespaces ON namespaces.id=projects.namespace_id")
- else
- Project.where(namespace: self_and_descendants)
- end
+ namespace = user? ? self : self_and_descendants
+ Project.where(namespace: namespace)
end
# Includes pipelines from this namespace and pipelines from all subgroups
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index 103c26289bf..aec008f1e40 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -14,12 +14,20 @@ module Projects
@key ||= DeployKey.new.tap { |dk| dk.deploy_keys_projects.build }
end
+ # It includes:
+ # - The deploy keys enabled in the project.
def enabled_keys
strong_memoize(:enabled_keys) do
project.deploy_keys.with_projects
end
end
+ # NOTE: This method is redundant. Use `available_project_keys` and `available_public_keys` instead.
+ # It includes:
+ # - Enabled deploy keys in projects that can be accessed by the user.
+ # - Instance-level public deploy keys.
+ # It excludes:
+ # - The deploy keys enabled in the project.
def available_keys
strong_memoize(:available_keys) do
current_user
@@ -29,22 +37,46 @@ module Projects
end
end
+ # It includes:
+ # - Enabled deploy keys in projects that can be accessed by the user.
+ # It excludes:
+ # - The deploy keys enabled in the project
def available_project_keys
- strong_memoize(:available_project_keys) do
- current_user
- .project_deploy_keys
- .id_not_in(enabled_keys.select(:id))
- .with_projects
+ if Feature.enabled?(:optimize_deploy_keys_presenter, project, default_enabled: :yaml)
+ strong_memoize(:available_project_keys) do
+ current_user.project_deploy_keys.with_projects - enabled_keys
+ end
+ else
+ strong_memoize(:legacy_available_project_keys) do
+ current_user
+ .project_deploy_keys
+ .id_not_in(enabled_keys.select(:id))
+ .with_projects
+ end
end
end
+ # It includes:
+ # - Instance-level public deploy keys.
+ # It excludes:
+ # - The deploy keys enabled in the project.
def available_public_keys
- strong_memoize(:available_public_keys) do
- DeployKey
- .are_public
- .id_not_in(enabled_keys.select(:id))
- .id_not_in(available_project_keys.select(:id))
- .with_projects
+ if Feature.enabled?(:optimize_deploy_keys_presenter, project, default_enabled: :yaml)
+ strong_memoize(:available_public_keys) do
+ DeployKey.are_public.with_projects - enabled_keys
+ end
+ else
+ strong_memoize(:legacy_available_public_keys) do
+ # This also excludes "Enabled deploy keys in projects that can be accessed by the user".
+ # However, this means we are filtering out a public key that enabled
+ # in the other project, which should be also available for this project.
+ # We should expose the public keys that has not been enabled on the project yet.
+ DeployKey
+ .are_public
+ .id_not_in(enabled_keys.select(:id))
+ .id_not_in(available_project_keys.select(:id))
+ .with_projects
+ end
end
end
@@ -78,10 +110,17 @@ module Projects
# rubocop: disable CodeReuse/ActiveRecord
def user_readable_project_ids
- project_ids = (available_keys + available_project_keys + available_public_keys)
- .flat_map { |deploy_key| deploy_key.deploy_keys_projects.map(&:project_id) }
- .compact
- .uniq
+ project_ids = if Feature.enabled?(:optimize_deploy_keys_presenter, project, default_enabled: :yaml)
+ (available_project_keys + available_public_keys)
+ .flat_map { |deploy_key| deploy_key.deploy_keys_projects.map(&:project_id) }
+ .compact
+ .uniq
+ else
+ (available_keys + available_project_keys + available_public_keys)
+ .flat_map { |deploy_key| deploy_key.deploy_keys_projects.map(&:project_id) }
+ .compact
+ .uniq
+ end
current_user.authorized_projects(Gitlab::Access::GUEST).id_in(project_ids).pluck(:id)
end
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index e8f2bb28d60..6cbdaeea5ea 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -36,6 +36,8 @@ class MemberEntity < Grape::Entity
GroupEntity.represent(member.source, only: [:id, :full_name, :web_url])
end
+ expose :type
+
expose :valid_level_roles, as: :valid_roles
expose :user, if: -> (member) { member.user.present? }, using: MemberUserEntity
diff --git a/app/services/boards/base_item_move_service.rb b/app/services/boards/base_item_move_service.rb
index bf3e29df54b..1a9d4e685b8 100644
--- a/app/services/boards/base_item_move_service.rb
+++ b/app/services/boards/base_item_move_service.rb
@@ -22,6 +22,12 @@ module Boards
)
end
+ reposition_ids = move_between_ids(params)
+ if reposition_ids
+ attrs[:move_between_ids] = reposition_ids
+ attrs.merge!(reposition_parent)
+ end
+
attrs
end
@@ -68,5 +74,13 @@ module Boards
Array(label_ids).compact
end
+
+ def move_between_ids(move_params)
+ ids = [move_params[:move_after_id], move_params[:move_before_id]]
+ .map(&:to_i)
+ .map { |m| m > 0 ? m : nil }
+
+ ids.any? ? ids : nil
+ end
end
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 99374fa01ae..76ea57968b2 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -3,8 +3,6 @@
module Boards
module Issues
class MoveService < Boards::BaseItemMoveService
- extend ::Gitlab::Utils::Override
-
def execute_multiple(issues)
return execute_multiple_empty_result if issues.empty?
@@ -57,25 +55,8 @@ module Boards
::Issues::UpdateService.new(issue.project, current_user, issue_modification_params).execute(issue)
end
- override :issuable_params
- def issuable_params(issuable)
- attrs = super
-
- move_between_ids = move_between_ids(params)
- if move_between_ids
- attrs[:move_between_ids] = move_between_ids
- attrs[:board_group_id] = board.group&.id
- end
-
- attrs
- end
-
- def move_between_ids(move_params)
- ids = [move_params[:move_after_id], move_params[:move_before_id]]
- .map(&:to_i)
- .map { |m| m > 0 ? m : nil }
-
- ids.any? ? ids : nil
+ def reposition_parent
+ { board_group_id: board.group&.id }
end
end
end
diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb
index 733aa75f255..73cf3308fe7 100644
--- a/app/services/ci/process_build_service.rb
+++ b/app/services/ci/process_build_service.rb
@@ -26,14 +26,6 @@ module Ci
end
def valid_statuses_for_build(build)
- if ::Feature.enabled?(:skip_dag_manual_and_delayed_jobs, build.project, default_enabled: :yaml)
- current_valid_statuses_for_build(build)
- else
- legacy_valid_statuses_for_build(build)
- end
- end
-
- def current_valid_statuses_for_build(build)
case build.when
when 'on_success', 'manual', 'delayed'
build.scheduling_type_dag? ? %w[success] : %w[success skipped]
@@ -45,23 +37,6 @@ module Ci
[]
end
end
-
- def legacy_valid_statuses_for_build(build)
- case build.when
- when 'on_success'
- build.scheduling_type_dag? ? %w[success] : %w[success skipped]
- when 'on_failure'
- %w[failed]
- when 'always'
- %w[success failed skipped]
- when 'manual'
- %w[success skipped]
- when 'delayed'
- %w[success skipped]
- else
- []
- end
- end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index f529a2d2ec7..f1ef2520485 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -424,6 +424,20 @@ class IssuableBaseService < BaseService
associations
end
+ def handle_move_between_ids(issuable_position)
+ return unless params[:move_between_ids]
+
+ after_id, before_id = params.delete(:move_between_ids)
+ positioning_scope_id = params.delete(positioning_scope_key)
+
+ issuable_before = issuable_for_positioning(before_id, positioning_scope_id)
+ issuable_after = issuable_for_positioning(after_id, positioning_scope_id)
+
+ raise ActiveRecord::RecordNotFound unless issuable_before || issuable_after
+
+ issuable_position.move_between(issuable_before, issuable_after)
+ end
+
def has_changes?(issuable, old_labels: [], old_assignees: [], old_reviewers: [])
valid_attrs = [:title, :description, :assignee_ids, :reviewer_ids, :milestone_id, :target_branch]
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 2906bdf62a7..f39655a6b07 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -96,19 +96,15 @@ module Issues
end
def handle_move_between_ids(issue)
- return unless params[:move_between_ids]
-
- after_id, before_id = params.delete(:move_between_ids)
- board_group_id = params.delete(:board_group_id)
-
- issue_before = get_issue_if_allowed(before_id, board_group_id)
- issue_after = get_issue_if_allowed(after_id, board_group_id)
- raise ActiveRecord::RecordNotFound unless issue_before || issue_after
+ super
- issue.move_between(issue_before, issue_after)
rebalance_if_needed(issue)
end
+ def positioning_scope_key
+ :board_group_id
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def change_issue_duplicate(issue)
canonical_issue_id = params.delete(:canonical_issue_id)
@@ -185,7 +181,7 @@ module Issues
end
# rubocop: disable CodeReuse/ActiveRecord
- def get_issue_if_allowed(id, board_group_id = nil)
+ def issuable_for_positioning(id, board_group_id = nil)
return unless id
issue =
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index 85855f45e33..64844a3f002 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -37,3 +37,5 @@ module Users
end
end
end
+
+Users::ActivityService.prepend_ee_mod
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 7ea10296d97..5e93b1d89eb 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,6 +1,6 @@
%main{ :role => "main" }
- .modal-no-backdrop.modal-doorkeepr-auth
- .modal-content
+ .modal-dialog.modal-doorkeepr-auth
+ .modal-content.gl-shadow-none
.modal-header
%h3.page-title
- link_to_client = link_to(@pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml
index 0462c29f5c1..2c9ffe3dc1d 100644
--- a/app/views/search/results/_empty.html.haml
+++ b/app/views/search/results/_empty.html.haml
@@ -1,4 +1,4 @@
-.search_box.gl-my-8
+.search_box.gl-my-8.gl-text-center
.search_glyph
%h4
= sprite_icon('search', size: 24, css_class: 'gl-vertical-align-text-bottom')
diff --git a/app/views/shared/_search_settings.html.haml b/app/views/shared/_search_settings.html.haml
index d689e9ae5c0..2974b2bf4d0 100644
--- a/app/views/shared/_search_settings.html.haml
+++ b/app/views/shared/_search_settings.html.haml
@@ -1,6 +1,5 @@
- container_class = local_assigns.fetch(:container_class, 'gl-mt-5')
-- if Feature.enabled?(:search_settings_in_page, @project, default_enabled: false)
- %div{ class: container_class }
- .js-search-settings-app
- %input.gl-form-input.form-control{ type: "text", placeholder: _("Search settings"), aria_label: _("Search settings"), disabled: true }
+%div{ class: container_class }
+ .js-search-settings-app
+ %input.gl-form-input.form-control{ type: "text", placeholder: _("Search settings"), aria_label: _("Search settings"), disabled: true }
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 0ba3e539357..483266cca86 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -120,7 +120,7 @@
= sprite_icon('leave', css_class: 'gl-icon')
= _('Leave')
- else
- %button{ data: { member_path: member_path(member.member), message: remove_member_message(member), is_access_request: member.request?.to_s, qa_selector: 'delete_member_button' },
+ %button{ data: { member_path: member_path(member.member), member_type: member.type, message: remove_member_message(member), is_access_request: member.request?.to_s, qa_selector: 'delete_member_button' },
class: "js-remove-member-button btn gl-button btn-danger align-self-center m-0 #{'ml-sm-2 btn-icon' unless force_mobile_view}",
title: remove_member_title(member) }
%span{ class: ('d-block d-sm-none' unless force_mobile_view) }
diff --git a/changelogs/unreleased/26522-commit-message-link-line-break.yml b/changelogs/unreleased/26522-commit-message-link-line-break.yml
new file mode 100644
index 00000000000..7a88270e493
--- /dev/null
+++ b/changelogs/unreleased/26522-commit-message-link-line-break.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed an issue where the link commit message did not end with a newline
+merge_request: 49086
+author: Kazuya Kojima
+type: fixed
diff --git a/changelogs/unreleased/294025-rollout-search-settings.yml b/changelogs/unreleased/294025-rollout-search-settings.yml
new file mode 100644
index 00000000000..8ffeb75efa6
--- /dev/null
+++ b/changelogs/unreleased/294025-rollout-search-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add in-page search for all settings pages
+merge_request: 56659
+author:
+type: added
diff --git a/changelogs/unreleased/297240-remove-skip_dag_manual_and_delayed_jobs.yml b/changelogs/unreleased/297240-remove-skip_dag_manual_and_delayed_jobs.yml
new file mode 100644
index 00000000000..6decd45cd26
--- /dev/null
+++ b/changelogs/unreleased/297240-remove-skip_dag_manual_and_delayed_jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the FF skip_dag_manual_and_delayed_jobs
+merge_request: 57086
+author:
+type: other
diff --git a/changelogs/unreleased/299685-fix-packages-build-info-when-pushed-with-job-token.yml b/changelogs/unreleased/299685-fix-packages-build-info-when-pushed-with-job-token.yml
new file mode 100644
index 00000000000..ed5d82251de
--- /dev/null
+++ b/changelogs/unreleased/299685-fix-packages-build-info-when-pushed-with-job-token.yml
@@ -0,0 +1,5 @@
+---
+title: Fix `#current_authenticated_job` when used with `.authenticate_with` in Grape APIs
+merge_request: 56564
+author:
+type: fixed
diff --git a/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze.yml b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze.yml
new file mode 100644
index 00000000000..1eac2bd442b
--- /dev/null
+++ b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary use of freeze
+merge_request: 57056
+author: Lee Tickett @leetickett
+type: other
diff --git a/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze2.yml b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze2.yml
new file mode 100644
index 00000000000..9fc55ed3204
--- /dev/null
+++ b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze2.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary use of freeze
+merge_request: 57057
+author: Lee Tickett @leetickett
+type: other
diff --git a/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze3.yml b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze3.yml
new file mode 100644
index 00000000000..5b11e74e773
--- /dev/null
+++ b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze3.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary use of freeze
+merge_request: 57058
+author: Lee Tickett @leetickett
+type: other
diff --git a/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze5.yml b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze5.yml
new file mode 100644
index 00000000000..15018468964
--- /dev/null
+++ b/changelogs/unreleased/31343-remove-unnecessary-use-of-freeze5.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary use of freeze
+merge_request: 57060
+author: Lee Tickett @leetickett
+type: other
diff --git a/changelogs/unreleased/fix-option-remove-memberships-from-subresources.yml b/changelogs/unreleased/fix-option-remove-memberships-from-subresources.yml
new file mode 100644
index 00000000000..392c161f093
--- /dev/null
+++ b/changelogs/unreleased/fix-option-remove-memberships-from-subresources.yml
@@ -0,0 +1,6 @@
+---
+title: 'Remove group member: add option to also remove direct user membership from
+ subgroups and projects'
+merge_request: 55980
+author: Jonas Wälter @wwwjon
+type: changed
diff --git a/changelogs/unreleased/pl-rubocop-todo-department-name.yml b/changelogs/unreleased/pl-rubocop-todo-department-name.yml
new file mode 100644
index 00000000000..ec65e654e7c
--- /dev/null
+++ b/changelogs/unreleased/pl-rubocop-todo-department-name.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes rubocop offense Migration/DepartmentName
+merge_request: 56997
+author: Shubham Kumar (@imskr)
+type: fixed
diff --git a/changelogs/unreleased/revert-inner-join-cte-fix.yml b/changelogs/unreleased/revert-inner-join-cte-fix.yml
new file mode 100644
index 00000000000..58764c78c04
--- /dev/null
+++ b/changelogs/unreleased/revert-inner-join-cte-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the recursive_namespace_lookup_as_inner_join feature flag
+merge_request: 57131
+author:
+type: other
diff --git a/changelogs/unreleased/sh-fix-encoded-api-project-urls.yml b/changelogs/unreleased/sh-fix-encoded-api-project-urls.yml
new file mode 100644
index 00000000000..c507c602084
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-encoded-api-project-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Workhorse acceleration for encoded project IDs in API
+merge_request: 56731
+author:
+type: performance
diff --git a/config/feature_flags/development/skip_dag_manual_and_delayed_jobs.yml b/config/feature_flags/development/ci_external_validation_service.yml
index 640be201868..9df770d87e5 100644
--- a/config/feature_flags/development/skip_dag_manual_and_delayed_jobs.yml
+++ b/config/feature_flags/development/ci_external_validation_service.yml
@@ -1,8 +1,8 @@
---
-name: skip_dag_manual_and_delayed_jobs
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50765
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297240
-milestone: '13.8'
+name: ci_external_validation_service
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56856
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323935
+milestone: '13.11'
type: development
-group: group::pipeline authoring
-default_enabled: true
+group: group::continuous integration
+default_enabled: false
diff --git a/config/feature_flags/development/ci_lower_frequency_trace_update.yml b/config/feature_flags/development/ci_lower_frequency_trace_update.yml
new file mode 100644
index 00000000000..396f2d25a39
--- /dev/null
+++ b/config/feature_flags/development/ci_lower_frequency_trace_update.yml
@@ -0,0 +1,8 @@
+---
+name: ci_lower_frequency_trace_update
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56743
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324768
+milestone: '13.11'
+type: development
+group: group::continuous integration
+default_enabled: false
diff --git a/config/feature_flags/development/search_settings_in_page.yml b/config/feature_flags/development/optimize_deploy_keys_presenter.yml
index 26db77ebdb7..6eee659856a 100644
--- a/config/feature_flags/development/search_settings_in_page.yml
+++ b/config/feature_flags/development/optimize_deploy_keys_presenter.yml
@@ -1,8 +1,8 @@
---
-name: search_settings_in_page
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50207
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/294025
-milestone: '13.7'
+name: optimize_deploy_keys_presenter
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56305/diffs
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324143
+milestone: '13.10'
type: development
-group: group::editor
+group: group::release
default_enabled: false
diff --git a/config/feature_flags/development/pipeline_graph_layers_view.yml b/config/feature_flags/development/pipeline_graph_layers_view.yml
new file mode 100644
index 00000000000..fae3e118226
--- /dev/null
+++ b/config/feature_flags/development/pipeline_graph_layers_view.yml
@@ -0,0 +1,8 @@
+---
+name: pipeline_graph_layers_view
+introduced_by_url:
+rollout_issue_url:
+milestone: '13.11'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/recursive_namespace_lookup_as_inner_join.yml b/config/feature_flags/development/recursive_namespace_lookup_as_inner_join.yml
deleted file mode 100644
index c28e553f23e..00000000000
--- a/config/feature_flags/development/recursive_namespace_lookup_as_inner_join.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: recursive_namespace_lookup_as_inner_join
-introduced_by_url:
-rollout_issue_url:
-milestone: '13.10'
-type: development
-group: group::optimize
-default_enabled: false
diff --git a/config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml b/config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml
new file mode 100644
index 00000000000..78e60987a7f
--- /dev/null
+++ b/config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml
@@ -0,0 +1,8 @@
+---
+name: gitlab_service_measuring_projects_create_service
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30977
+rollout_issue_url:
+milestone: '13.0'
+type: ops
+group: group::memory
+default_enabled: false
diff --git a/config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml b/config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml
new file mode 100644
index 00000000000..309492f8be9
--- /dev/null
+++ b/config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml
@@ -0,0 +1,8 @@
+---
+name: gitlab_service_measuring_projects_import_export_export_service
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30977
+rollout_issue_url:
+milestone: '13.0'
+type: ops
+group: group::memory
+default_enabled: false
diff --git a/config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml b/config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml
new file mode 100644
index 00000000000..03a8eca99d9
--- /dev/null
+++ b/config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml
@@ -0,0 +1,8 @@
+---
+name: gitlab_service_measuring_projects_import_service
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30977
+rollout_issue_url:
+milestone: '13.0'
+type: ops
+group: group::memory
+default_enabled: false
diff --git a/config/feature_flags/ops/redis_hll_tracking.yml b/config/feature_flags/ops/redis_hll_tracking.yml
new file mode 100644
index 00000000000..6570143d60d
--- /dev/null
+++ b/config/feature_flags/ops/redis_hll_tracking.yml
@@ -0,0 +1,8 @@
+---
+name: redis_hll_tracking
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56970
+rollout_issue_url:
+milestone: '13.11'
+type: ops
+group: group::product intelligence
+default_enabled: true
diff --git a/config/feature_flags/ops/x509_forced_cert_loading.yml b/config/feature_flags/ops/x509_forced_cert_loading.yml
new file mode 100644
index 00000000000..b884a5b47bc
--- /dev/null
+++ b/config/feature_flags/ops/x509_forced_cert_loading.yml
@@ -0,0 +1,8 @@
+---
+name: x509_forced_cert_loading
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54569
+rollout_issue_url:
+milestone: '13.10'
+type: ops
+group: group::source code
+default_enabled: false
diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md
index 64426a60ab0..44cbb626f0c 100644
--- a/doc/administration/external_pipeline_validation.md
+++ b/doc/administration/external_pipeline_validation.md
@@ -22,12 +22,12 @@ invalidated.
Response Code Legend:
- `200` - Accepted
-- `4xx` - Not Accepted
+- `406` - Not Accepted
- Other Codes - Accepted and Logged
## Configuration
-Set the `EXTERNAL_VALIDATION_SERVICE_URL` to the external service URL.
+Set the `EXTERNAL_VALIDATION_SERVICE_URL` to the external service URL and enable `ci_external_validation_service` feature flag.
## Payload Schema
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index d028fd2264c..6fba192e794 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -171,7 +171,7 @@ URL scheme: `https://<namespace>.example.io/<project_slug>`
NGINX proxies all requests to the daemon. Pages daemon doesn't listen to the
outside world.
-1. Place the certificate and key inside `/etc/gitlab/ssl`
+1. Place the `example.io` certificate and key inside `/etc/gitlab/ssl`.
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
```ruby
@@ -189,6 +189,9 @@ then you'll need to also add the full paths as shown below:
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. If you're using [Pages Access Control](#access-control), update the redirect URI in the GitLab Pages
+[System OAuth application](../../integration/oauth_provider.md#instance-wide-applications)
+to use the HTTPS protocol.
WARNING:
Multiple wildcards for one instance is not supported. Only one wildcard per instance can be assigned.
@@ -303,7 +306,7 @@ In that case, the Pages daemon is running, NGINX still proxies requests to
the daemon but the daemon is also able to receive requests from the outside
world. Custom domains are supported, but no TLS.
-1. Edit `/etc/gitlab/gitlab.rb`:
+1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
```ruby
pages_external_url "http://example.io"
@@ -334,23 +337,35 @@ In that case, the Pages daemon is running, NGINX still proxies requests to
the daemon but the daemon is also able to receive requests from the outside
world. Custom domains and TLS are supported.
-1. Edit `/etc/gitlab/gitlab.rb`:
+1. Place the `example.io` certificate and key inside `/etc/gitlab/ssl`.
+1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
```ruby
pages_external_url "https://example.io"
nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
- gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
- gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001:db8::2]:80']
gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001:db8::2]:443']
+ # Redirect pages from HTTP to HTTPS
+ gitlab_pages['redirect_http'] = true
```
where `192.0.2.1` is the primary IP address that GitLab is listening to and
`192.0.2.2` and `2001:db8::2` are the secondary IPs where the GitLab Pages daemon
listens on. If you don't have IPv6, you can omit the IPv6 address.
+1. If you haven't named your certificate and key `example.io.crt` and `example.io.key` respectively,
+then you'll need to also add the full paths as shown below:
+
+ ```ruby
+ gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
+ gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
+ ```
+
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. If you're using [Pages Access Control](#access-control), update the redirect URI in the GitLab Pages
+[System OAuth application](../../integration/oauth_provider.md#instance-wide-applications)
+to use the HTTPS protocol.
### Custom domain verification
@@ -1115,3 +1130,8 @@ gitlab_pages['env'] = {'TMPDIR' => '<new_tmp_path>'}
Once added, reconfigure with `sudo gitlab-ctl reconfigure` and restart GitLab with
`sudo gitlab-ctl restart`.
+
+### `The redirect URI included is not valid.` when using Pages Access Control
+
+Verify that the **Callback URL**/Redirect URI in the GitLab Pages [System OAuth application](../../integration/oauth_provider.md#instance-wide-applications)
+is using the protocol (HTTP or HTTPS) that `pages_external_url` is configured to use.
diff --git a/doc/api/members.md b/doc/api/members.md
index 286be10dd6e..b9070b8f305 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -494,7 +494,10 @@ DELETE /projects/:id/members/:user_id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the member |
-| `unassign_issuables` | boolean | false | Flag indicating if the removed member should be unassigned from any issues or merge requests inside a given group or project |
+| `skip_subresources` | boolean | false | Whether the deletion of direct memberships of the removed member in subgroups and projects should be skipped. Default is `false`. |
+| `unassign_issuables` | boolean | false | Whether the removed member should be unassigned from any issues or merge requests inside a given group or project. Default is `false`. |
+
+Example request:
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/members/:user_id"
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 9fcf4347bdd..0d1897b7235 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -1437,6 +1437,34 @@ describe('My Index test with `createMockApollo`', () => {
});
```
+When you need to configure the mocked apollo client's caching behavior,
+provide additional cache options when creating a mocked client instance and the provided options will merge with the default cache option:
+
+```javascript
+const defaultCacheOptions = {
+ fragmentMatcher: { match: () => true },
+ addTypename: false,
+};
+```
+
+```javascript
+function createMockApolloProvider({ props = {}, requestHandlers } = {}) {
+ Vue.use(VueApollo);
+
+ const mockApollo = createMockApollo(
+ requestHandlers,
+ {},
+ {
+ dataIdFromObject: (object) =>
+ // eslint-disable-next-line no-underscore-dangle
+ object.__typename === 'Requirement' ? object.iid : defaultDataIdFromObject(object),
+ },
+ );
+
+ return mockApollo;
+}
+```
+
## Handling errors
The GitLab GraphQL mutations have two distinct error modes: [Top-level](#top-level-errors) and [errors-as-data](#errors-as-data).
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index e9a7903e18e..92b616c5549 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -302,37 +302,12 @@ GitLab instance.
## Search settings
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292941) in GitLab 13.8.
-> - [Added to Group, Admin, and User settings](https://gitlab.com/groups/gitlab-org/-/epics/4842) in GitLab 13.9
-> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
-> - It's disabled on GitLab.com.
-> - It's not recommended for production use.
-> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-search-settings). **(FREE SELF)**
-
-WARNING:
-This feature might not be available to you. Check the **version history** note above for details.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292941) in GitLab 13.8 behind a feature flag, disabled by default.
+> - [Added to Group, Admin, and User settings](https://gitlab.com/groups/gitlab-org/-/epics/4842) in GitLab 13.9.
+> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/294025) in GitLab 13.11.
You can search inside a Project, Group, Admin, or User's settings by entering
a search term in the search box located at the top of the page. The search results
appear highlighted in the sections that match the search term.
![Search project settings](img/project_search_general_settings_v13_8.png)
-
-### Enable or disable Search settings **(FREE SELF)**
-
-Search settings is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can enable it.
-
-To enable it:
-
-```ruby
-Feature.enable(:search_settings_in_page)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:search_settings_in_page)
-```
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 1c6ff23dde6..51942088262 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -49,7 +49,11 @@ module API
# Returns the job associated with the token provided for
# authentication, if any
def current_authenticated_job
- @current_authenticated_job
+ if try(:namespace_inheritable, :authentication)
+ ci_build_from_namespace_inheritable
+ else
+ @current_authenticated_job # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
diff --git a/lib/api/helpers/authentication.rb b/lib/api/helpers/authentication.rb
index a6cfe930190..da11f07485b 100644
--- a/lib/api/helpers/authentication.rb
+++ b/lib/api/helpers/authentication.rb
@@ -52,6 +52,11 @@ module API
token&.user
end
+ def ci_build_from_namespace_inheritable
+ token = token_from_namespace_inheritable
+ token if token.is_a?(::Ci::Build)
+ end
+
private
def find_token_from_raw_credentials(token_types, raw)
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 7800a9c0248..6332e5e9a17 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -23,7 +23,7 @@ module API
helpers ::API::Helpers::InternalHelpers
- UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result'.freeze
+ UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result'
VALID_PAT_SCOPES = Set.new(
Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth::REGISTRY_SCOPES
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 42f608102b3..7cddeb50a25 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -155,6 +155,8 @@ module API
desc 'Removes a user from a group or project.'
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
+ optional :skip_subresources, type: Boolean, default: false,
+ desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped'
optional :unassign_issuables, type: Boolean, default: false,
desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project'
end
@@ -164,7 +166,7 @@ module API
member = source_members(source).find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
- ::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables])
+ ::Members::DestroyService.new(current_user).execute(member, skip_subresources: params[:skip_subresources], unassign_issuables: params[:unassign_issuables])
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb
index 2d25e76626a..3a0609651f4 100644
--- a/lib/api/v3/github.rb
+++ b/lib/api/v3/github.rb
@@ -18,7 +18,7 @@ module API
# Used to differentiate Jira Cloud requests from Jira Server requests
# Jira Cloud user agent format: Jira DVCS Connector Vertigo/version
# Jira Server user agent format: Jira DVCS Connector/version
- JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'.freeze
+ JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'
include PaginationParams
diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb
index 5288db3b0cb..a615abc1989 100644
--- a/lib/banzai/filter/commit_trailers_filter.rb
+++ b/lib/banzai/filter/commit_trailers_filter.rb
@@ -36,7 +36,7 @@ module Banzai
next if html == content
- node.replace(html)
+ node.replace("\n\n#{html}")
end
doc
diff --git a/lib/bulk_imports/clients/http.rb b/lib/bulk_imports/clients/http.rb
index 2e81863e53a..acc94d5ff2e 100644
--- a/lib/bulk_imports/clients/http.rb
+++ b/lib/bulk_imports/clients/http.rb
@@ -3,7 +3,7 @@
module BulkImports
module Clients
class Http
- API_VERSION = 'v4'.freeze
+ API_VERSION = 'v4'
DEFAULT_PAGE = 1.freeze
DEFAULT_PER_PAGE = 30.freeze
diff --git a/lib/csv_builder.rb b/lib/csv_builder.rb
index 6116009f171..43ceed9519b 100644
--- a/lib/csv_builder.rb
+++ b/lib/csv_builder.rb
@@ -14,7 +14,7 @@
# CsvBuilder.new(@posts, columns).render
#
class CsvBuilder
- DEFAULT_ORDER_BY = 'id'.freeze
+ DEFAULT_ORDER_BY = 'id'
DEFAULT_BATCH_SIZE = 1000
PREFIX_REGEX = /^[=\+\-@;]/.freeze
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index 4c6254c9e69..6f6ac79c16b 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -24,9 +24,9 @@ module Gitlab
PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'
PRIVATE_TOKEN_PARAM = :private_token
- JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
+ JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'
JOB_TOKEN_PARAM = :job_token
- DEPLOY_TOKEN_HEADER = 'HTTP_DEPLOY_TOKEN'.freeze
+ DEPLOY_TOKEN_HEADER = 'HTTP_DEPLOY_TOKEN'
RUNNER_TOKEN_PARAM = :token
RUNNER_JOB_TOKEN_PARAM = :token
diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb
index d056501a6d3..b2fbe43aa77 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/external.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb
@@ -11,8 +11,13 @@ module Gitlab
InvalidResponseCode = Class.new(StandardError)
VALIDATION_REQUEST_TIMEOUT = 5
+ ACCEPTED_STATUS = 200
+ DOT_COM_REJECTED_STATUS = 406
+ GENERAL_REJECTED_STATUS = (400..499).freeze
def perform!
+ return unless enabled?
+
pipeline_authorized = validate_external
log_message = pipeline_authorized ? 'authorized' : 'not authorized'
@@ -27,27 +32,42 @@ module Gitlab
private
+ def enabled?
+ return true unless Gitlab.com?
+
+ ::Feature.enabled?(:ci_external_validation_service, project, default_enabled: :yaml)
+ end
+
def validate_external
return true unless validation_service_url
# 200 - accepted
- # 4xx - not accepted
+ # 406 - not accepted on GitLab.com
+ # 4XX - not accepted for other installations
# everything else - accepted and logged
response_code = validate_service_request.code
case response_code
- when 200
+ when ACCEPTED_STATUS
true
- when 400..499
+ when rejected_status
false
else
raise InvalidResponseCode, "Unsupported response code received from Validation Service: #{response_code}"
end
rescue => ex
- Gitlab::ErrorTracking.track_exception(ex)
+ Gitlab::ErrorTracking.track_exception(ex, project_id: project.id)
true
end
+ def rejected_status
+ if Gitlab.com?
+ DOT_COM_REJECTED_STATUS
+ else
+ GENERAL_REJECTED_STATUS
+ end
+ end
+
def validate_service_request
Gitlab::HTTP.post(
validation_service_url, timeout: VALIDATION_REQUEST_TIMEOUT,
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 3258d965c93..a177f56f434 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -11,7 +11,8 @@ module Gitlab
LOCK_SLEEP = 0.001.seconds
WATCH_FLAG_TTL = 10.seconds
- UPDATE_FREQUENCY_DEFAULT = 30.seconds
+ LEGACY_UPDATE_FREQUENCY_DEFAULT = 30.seconds
+ UPDATE_FREQUENCY_DEFAULT = 60.seconds
UPDATE_FREQUENCY_WHEN_BEING_WATCHED = 3.seconds
ArchiveError = Class.new(StandardError)
@@ -114,7 +115,15 @@ module Gitlab
end
def update_interval
- being_watched? ? UPDATE_FREQUENCY_WHEN_BEING_WATCHED : UPDATE_FREQUENCY_DEFAULT
+ if being_watched?
+ UPDATE_FREQUENCY_WHEN_BEING_WATCHED
+ else
+ if Feature.enabled?(:ci_lower_frequency_trace_update, job.project, default_enabled: :yaml)
+ UPDATE_FREQUENCY_DEFAULT
+ else
+ LEGACY_UPDATE_FREQUENCY_DEFAULT
+ end
+ end
end
def being_watched!
diff --git a/lib/gitlab/conan_token.rb b/lib/gitlab/conan_token.rb
index d03997b4158..c3d90aa78fb 100644
--- a/lib/gitlab/conan_token.rb
+++ b/lib/gitlab/conan_token.rb
@@ -7,7 +7,7 @@
module Gitlab
class ConanToken
- HMAC_KEY = 'gitlab-conan-packages'.freeze
+ HMAC_KEY = 'gitlab-conan-packages'
attr_reader :access_token_id, :user_id
diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb
index e780bf8a986..f5f142c251f 100644
--- a/lib/gitlab/health_checks/gitaly_check.rb
+++ b/lib/gitlab/health_checks/gitaly_check.rb
@@ -5,7 +5,7 @@ module Gitlab
class GitalyCheck
extend BaseAbstractCheck
- METRIC_PREFIX = 'gitaly_health_check'.freeze
+ METRIC_PREFIX = 'gitaly_health_check'
class << self
def readiness
diff --git a/lib/gitlab/http_connection_adapter.rb b/lib/gitlab/http_connection_adapter.rb
index 37f618ae879..f7a3da53fdb 100644
--- a/lib/gitlab/http_connection_adapter.rb
+++ b/lib/gitlab/http_connection_adapter.rb
@@ -17,14 +17,6 @@ module Gitlab
def connection
@uri, hostname = validate_url!(uri)
- if options.key?(:http_proxyaddr)
- proxy_uri_with_port = uri_with_port(options[:http_proxyaddr], options[:http_proxyport])
- proxy_uri_validated = validate_url!(proxy_uri_with_port).first
-
- @options[:http_proxyaddr] = proxy_uri_validated.omit(:port).to_s
- @options[:http_proxyport] = proxy_uri_validated.port
- end
-
super.tap do |http|
http.hostname_override = hostname if hostname
end
@@ -53,11 +45,5 @@ module Gitlab
def allow_settings_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
-
- def uri_with_port(address, port)
- uri = Addressable::URI.parse(address)
- uri.port = port if port.present?
- uri
- end
end
end
diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb
index 33e709360ad..98e87e9e915 100644
--- a/lib/gitlab/pages.rb
+++ b/lib/gitlab/pages.rb
@@ -3,7 +3,7 @@
module Gitlab
module Pages
VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze
- INTERNAL_API_REQUEST_HEADER = 'Gitlab-Pages-Api-Request'.freeze
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Pages-Api-Request'
MAX_SIZE = 1.terabyte
include JwtAuthenticatable
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 5dc3f71329d..2b84789afc4 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -711,6 +711,8 @@ module Gitlab
end
def redis_hll_counters
+ return {} unless Feature.enabled?(:redis_hll_tracking, type: :ops, default_enabled: :yaml)
+
{ redis_hll_counters: ::Gitlab::UsageDataCounters::HLLRedisCounter.unique_events_data }
end
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index 336bef081a6..0249d1a12cd 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -157,7 +157,7 @@ module Gitlab
def feature_enabled?(event)
return true if event[:feature_flag].blank?
- Feature.enabled?(event[:feature_flag], default_enabled: :yaml)
+ Feature.enabled?(event[:feature_flag], default_enabled: :yaml) && Feature.enabled?(:redis_hll_tracking, type: :ops, default_enabled: :yaml)
end
# Allow to add totals for events that are in the same redis slot, category and have the same aggregation level
diff --git a/lib/learn_gitlab.rb b/lib/learn_gitlab.rb
index 771083193d1..abceb80bd30 100644
--- a/lib/learn_gitlab.rb
+++ b/lib/learn_gitlab.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
class LearnGitlab
- PROJECT_NAME = 'Learn GitLab'.freeze
- BOARD_NAME = 'GitLab onboarding'.freeze
- LABEL_NAME = 'Novice'.freeze
+ PROJECT_NAME = 'Learn GitLab'
+ BOARD_NAME = 'GitLab onboarding'
+ LABEL_NAME = 'Novice'
def initialize(current_user)
@current_user = current_user
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4e4e648e61a..9cba563a8bf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -402,6 +402,9 @@ msgstr ""
msgid "%{board_target} not found"
msgstr ""
+msgid "%{codeStart}needs:%{codeEnd} relationships"
+msgstr ""
+
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr ""
@@ -3211,6 +3214,9 @@ msgstr ""
msgid "Also called \"Relying party service URL\" or \"Reply URL\""
msgstr ""
+msgid "Also remove direct user membership from subgroups and projects"
+msgstr ""
+
msgid "Also unassign this user from related issues and merge requests"
msgstr ""
@@ -21681,6 +21687,9 @@ msgstr ""
msgid "Or you can choose one of the suggested colors below"
msgstr ""
+msgid "Order jobs by"
+msgstr ""
+
msgid "Orphaned member"
msgstr ""
@@ -33014,7 +33023,10 @@ msgstr ""
msgid "User was successfully created."
msgstr ""
-msgid "User was successfully removed from group and any subresources."
+msgid "User was successfully removed from group and any subgroups and projects."
+msgstr ""
+
+msgid "User was successfully removed from group."
msgstr ""
msgid "User was successfully removed from project."
@@ -33501,6 +33513,9 @@ msgstr ""
msgid "View the documentation"
msgstr ""
+msgid "View the jobs grouped into stages"
+msgstr ""
+
msgid "View the latest successful deployment to this environment"
msgstr ""
@@ -33510,6 +33525,9 @@ msgstr ""
msgid "View users statistics"
msgstr ""
+msgid "View what jobs are needed for a job to run"
+msgstr ""
+
msgid "Viewed"
msgstr ""
diff --git a/rubocop/cop/migration/hash_index.rb b/rubocop/cop/migration/hash_index.rb
index dba202ef0e3..8becef891af 100644
--- a/rubocop/cop/migration/hash_index.rb
+++ b/rubocop/cop/migration/hash_index.rb
@@ -11,7 +11,7 @@ module RuboCop
include MigrationHelpers
MSG = 'hash indexes should be avoided at all costs since they are not ' \
- 'recorded in the PostgreSQL WAL, you should use a btree index instead'.freeze
+ 'recorded in the PostgreSQL WAL, you should use a btree index instead'
NAMES = Set.new([:add_index, :index, :add_concurrent_index]).freeze
diff --git a/rubocop/cop/migration/prevent_strings.rb b/rubocop/cop/migration/prevent_strings.rb
index bfeabd2c78d..57e29bf74ae 100644
--- a/rubocop/cop/migration/prevent_strings.rb
+++ b/rubocop/cop/migration/prevent_strings.rb
@@ -11,7 +11,7 @@ module RuboCop
MSG = 'Do not use the `string` data type, use `text` instead. ' \
'Updating limits on strings requires downtime. This can be avoided ' \
- 'by using `text` and adding a limit with `add_text_limit`'.freeze
+ 'by using `text` and adding a limit with `add_text_limit`'
def_node_matcher :reverting?, <<~PATTERN
(def :down ...)
diff --git a/rubocop/cop/migration/remove_column.rb b/rubocop/cop/migration/remove_column.rb
index f63df71467c..6a171ac948f 100644
--- a/rubocop/cop/migration/remove_column.rb
+++ b/rubocop/cop/migration/remove_column.rb
@@ -10,7 +10,7 @@ module RuboCop
class RemoveColumn < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`remove_column` must only be used in post-deployment migrations'.freeze
+ MSG = '`remove_column` must only be used in post-deployment migrations'
def on_def(node)
def_method = node.children[0]
diff --git a/rubocop/cop/migration/remove_concurrent_index.rb b/rubocop/cop/migration/remove_concurrent_index.rb
index 8c2c6fb157e..30dd59d97bc 100644
--- a/rubocop/cop/migration/remove_concurrent_index.rb
+++ b/rubocop/cop/migration/remove_concurrent_index.rb
@@ -11,7 +11,7 @@ module RuboCop
include MigrationHelpers
MSG = '`remove_concurrent_index` is not reversible so you must manually define ' \
- 'the `up` and `down` methods in your migration class, using `add_concurrent_index` in `down`'.freeze
+ 'the `up` and `down` methods in your migration class, using `add_concurrent_index` in `down`'
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/remove_index.rb b/rubocop/cop/migration/remove_index.rb
index 15c2f37b4b0..ca5d4af1520 100644
--- a/rubocop/cop/migration/remove_index.rb
+++ b/rubocop/cop/migration/remove_index.rb
@@ -9,7 +9,7 @@ module RuboCop
class RemoveIndex < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`remove_index` requires downtime, use `remove_concurrent_index` instead'.freeze
+ MSG = '`remove_index` requires downtime, use `remove_concurrent_index` instead'
def on_def(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/safer_boolean_column.rb b/rubocop/cop/migration/safer_boolean_column.rb
index 06bb24707bd..1d780d96afa 100644
--- a/rubocop/cop/migration/safer_boolean_column.rb
+++ b/rubocop/cop/migration/safer_boolean_column.rb
@@ -21,9 +21,9 @@ module RuboCop
class SaferBooleanColumn < RuboCop::Cop::Cop
include MigrationHelpers
- DEFAULT_OFFENSE = 'Boolean columns on the `%s` table should have a default. You may wish to use `add_column_with_default`.'.freeze
- NULL_OFFENSE = 'Boolean columns on the `%s` table should disallow nulls.'.freeze
- DEFAULT_AND_NULL_OFFENSE = 'Boolean columns on the `%s` table should have a default and should disallow nulls. You may wish to use `add_column_with_default`.'.freeze
+ DEFAULT_OFFENSE = 'Boolean columns on the `%s` table should have a default. You may wish to use `add_column_with_default`.'
+ NULL_OFFENSE = 'Boolean columns on the `%s` table should disallow nulls.'
+ DEFAULT_AND_NULL_OFFENSE = 'Boolean columns on the `%s` table should have a default and should disallow nulls. You may wish to use `add_column_with_default`.'
def_node_matcher :add_column?, <<~PATTERN
(send nil? :add_column $...)
diff --git a/rubocop/cop/migration/timestamps.rb b/rubocop/cop/migration/timestamps.rb
index 5584d49ee8c..44baf17d968 100644
--- a/rubocop/cop/migration/timestamps.rb
+++ b/rubocop/cop/migration/timestamps.rb
@@ -9,7 +9,7 @@ module RuboCop
class Timestamps < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = 'Do not use `timestamps`, use `timestamps_with_timezone` instead'.freeze
+ MSG = 'Do not use `timestamps`, use `timestamps_with_timezone` instead'
# Check methods in table creation.
def on_def(node)
diff --git a/rubocop/cop/migration/update_column_in_batches.rb b/rubocop/cop/migration/update_column_in_batches.rb
index d23e0d28380..e23042e1b9f 100644
--- a/rubocop/cop/migration/update_column_in_batches.rb
+++ b/rubocop/cop/migration/update_column_in_batches.rb
@@ -11,7 +11,7 @@ module RuboCop
include MigrationHelpers
MSG = 'Migration running `update_column_in_batches` must have a spec file at' \
- ' `%s`.'.freeze
+ ' `%s`.'
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/with_lock_retries_with_change.rb b/rubocop/cop/migration/with_lock_retries_with_change.rb
index 36fc1f92833..9d11edcb6a1 100644
--- a/rubocop/cop/migration/with_lock_retries_with_change.rb
+++ b/rubocop/cop/migration/with_lock_retries_with_change.rb
@@ -10,7 +10,7 @@ module RuboCop
include MigrationHelpers
MSG = '`with_lock_retries` cannot be used within `change` so you must manually define ' \
- 'the `up` and `down` methods in your migration class and use `with_lock_retries` in both methods'.freeze
+ 'the `up` and `down` methods in your migration class and use `with_lock_retries` in both methods'
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/usage_data/distinct_count_by_large_foreign_key.rb b/rubocop/cop/usage_data/distinct_count_by_large_foreign_key.rb
index 9fdf52dac8b..3aad089d961 100644
--- a/rubocop/cop/usage_data/distinct_count_by_large_foreign_key.rb
+++ b/rubocop/cop/usage_data/distinct_count_by_large_foreign_key.rb
@@ -13,7 +13,7 @@ module RuboCop
# distinct_count(Ci::Build, :commit_id)
#
class DistinctCountByLargeForeignKey < RuboCop::Cop::Cop
- MSG = 'Avoid doing `%s` on foreign keys for large tables having above 100 million rows.'.freeze
+ MSG = 'Avoid doing `%s` on foreign keys for large tables having above 100 million rows.'
def_node_matcher :distinct_count?, <<-PATTERN
(send _ $:distinct_count $...)
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index ff7a7f55863..aa9fd3759e5 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -288,7 +288,9 @@ RSpec.describe Groups::GroupMembersController do
end
describe 'DELETE destroy' do
- let(:member) { create(:group_member, :developer, group: group) }
+ let(:sub_group) { create(:group, parent: group) }
+ let!(:member) { create(:group_member, :developer, group: group) }
+ let!(:sub_member) { create(:group_member, :developer, group: sub_group, user: member.user) }
before do
sign_in(user)
@@ -324,9 +326,19 @@ RSpec.describe Groups::GroupMembersController do
it '[HTML] removes user from members' do
delete :destroy, params: { group_id: group, id: member }
- expect(response).to set_flash.to 'User was successfully removed from group and any subresources.'
+ expect(response).to set_flash.to 'User was successfully removed from group.'
expect(response).to redirect_to(group_group_members_path(group))
expect(group.members).not_to include member
+ expect(sub_group.members).to include sub_member
+ end
+
+ it '[HTML] removes user from members including subgroups and projects' do
+ delete :destroy, params: { group_id: group, id: member, remove_sub_memberships: true }
+
+ expect(response).to set_flash.to 'User was successfully removed from group and any subgroups and projects.'
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.members).not_to include member
+ expect(sub_group.members).not_to include sub_member
end
it '[JS] removes user from members' do
diff --git a/spec/features/admin/admin_search_settings_spec.rb b/spec/features/admin/admin_search_settings_spec.rb
index a78d17a6651..cd61a1db6f3 100644
--- a/spec/features/admin/admin_search_settings_spec.rb
+++ b/spec/features/admin/admin_search_settings_spec.rb
@@ -20,8 +20,10 @@ RSpec.describe 'Admin searches application settings', :js do
end
context 'in ci/cd settings page' do
- let(:visit_path) { ci_cd_admin_application_settings_path }
+ before do
+ visit(ci_cd_admin_application_settings_path)
+ end
- it_behaves_like 'can search settings with feature flag check', 'Variables', 'Package Registry'
+ it_behaves_like 'can search settings', 'Variables', 'Package Registry'
end
end
diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb
index cad49507323..82383ece2d3 100644
--- a/spec/features/boards/sidebar_assignee_spec.rb
+++ b/spec/features/boards/sidebar_assignee_spec.rb
@@ -2,19 +2,19 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards', :js do
+RSpec.describe 'Project issue boards sidebar assignee', :js do
include BoardHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let!(:development) { create(:label, project: project, name: 'Development') }
- let!(:regression) { create(:label, project: project, name: 'Regression') }
- let!(:stretch) { create(:label, project: project, name: 'Stretch') }
- let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [development], relative_position: 2) }
- let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
- let(:board) { create(:board, project: project) }
- let!(:list) { create(:list, board: board, label: development, position: 0) }
- let(:card) { find('.board:nth-child(2)').first('.board-card') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:development) { create(:label, project: project, name: 'Development') }
+ let_it_be(:regression) { create(:label, project: project, name: 'Regression') }
+ let_it_be(:stretch) { create(:label, project: project, name: 'Stretch') }
+ let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [development], relative_position: 2) }
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
+ let(:board) { create(:board, project: project) }
+ let!(:list) { create(:list, board: board, label: development, position: 0) }
+ let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
project.add_maintainer(user)
diff --git a/spec/features/boards/sidebar_due_date_spec.rb b/spec/features/boards/sidebar_due_date_spec.rb
new file mode 100644
index 00000000000..f2d51fb56a7
--- /dev/null
+++ b/spec/features/boards/sidebar_due_date_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project issue boards sidebar due date', :js do
+ include BoardHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project, relative_position: 1) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:list) { create(:list, board: board, position: 0) }
+ let(:card) { find('.board:nth-child(1)').first('.board-card') }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_maintainer(user)
+
+ sign_in(user)
+
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
+ context 'due date' do
+ it 'updates due date' do
+ click_card(card)
+
+ page.within('.due_date') do
+ today = Date.today.day
+
+ click_link 'Edit'
+
+ click_button today.to_s
+
+ wait_for_requests
+
+ expect(page).to have_content(today.to_s(:medium))
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 0648bb67787..d6e908698c6 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -2,20 +2,20 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards', :js do
+RSpec.describe 'Project issue boards sidebar labels', :js do
include BoardHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let!(:development) { create(:label, project: project, name: 'Development') }
- let!(:bug) { create(:label, project: project, name: 'Bug') }
- let!(:regression) { create(:label, project: project, name: 'Regression') }
- let!(:stretch) { create(:label, project: project, name: 'Stretch') }
- let!(:issue1) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
- let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
- let(:board) { create(:board, project: project) }
- let!(:list) { create(:list, board: board, label: development, position: 0) }
- let(:card) { find('.board:nth-child(2)').first('.board-card') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:development) { create(:label, project: project, name: 'Development') }
+ let_it_be(:bug) { create(:label, project: project, name: 'Bug') }
+ let_it_be(:regression) { create(:label, project: project, name: 'Regression') }
+ let_it_be(:stretch) { create(:label, project: project, name: 'Stretch') }
+ let_it_be(:issue1) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
+ let_it_be(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:list) { create(:list, board: board, label: development, position: 0) }
+ let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
project.add_maintainer(user)
diff --git a/spec/features/boards/sidebar_milestones_spec.rb b/spec/features/boards/sidebar_milestones_spec.rb
new file mode 100644
index 00000000000..d4f130ba3ee
--- /dev/null
+++ b/spec/features/boards/sidebar_milestones_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project issue boards sidebar milestones', :js do
+ include BoardHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:issue1) { create(:issue, project: project, relative_position: 1) }
+ let_it_be(:issue2) { create(:issue, project: project, milestone: milestone, relative_position: 2) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:list) { create(:list, board: board, position: 0) }
+ let(:card1) { find('.board:nth-child(1) .board-card:nth-of-type(1)') }
+ let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
+
+ before do
+ project.add_maintainer(user)
+
+ sign_in(user)
+
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
+ context 'milestone' do
+ it 'adds a milestone' do
+ click_card(card1)
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_requests
+
+ click_link milestone.title
+
+ wait_for_requests
+
+ page.within('.value') do
+ expect(page).to have_content(milestone.title)
+ end
+ end
+ end
+
+ it 'removes a milestone' do
+ click_card(card2)
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_requests
+
+ click_link "No milestone"
+
+ wait_for_requests
+
+ page.within('.value') do
+ expect(page).not_to have_content(milestone.title)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 5aeb5cff15f..4c93707cc44 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -2,34 +2,21 @@
require 'spec_helper'
-RSpec.describe 'Issue Boards', :js do
+RSpec.describe 'Project issue boards sidebar', :js do
include BoardHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let!(:milestone) { create(:milestone, project: project) }
- let!(:development) { create(:label, project: project, name: 'Development') }
- let!(:regression) { create(:label, project: project, name: 'Regression') }
- let!(:stretch) { create(:label, project: project, name: 'Stretch') }
- let!(:issue1) { create(:labeled_issue, project: project, milestone: milestone, labels: [development], relative_position: 2) }
- let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
- let(:board) { create(:board, project: project) }
- let!(:list) { create(:list, board: board, label: development, position: 0) }
- let(:card) { find('.board:nth-child(2)').first('.board-card') }
-
- let(:application_settings) { {} }
-
- around do |example|
- freeze_time { example.run }
- end
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project, relative_position: 1) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:list) { create(:list, board: board, position: 0) }
+ let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do
project.add_maintainer(user)
sign_in(user)
- stub_application_setting(application_settings)
-
visit project_board_path(project, board)
wait_for_requests
end
@@ -64,115 +51,8 @@ RSpec.describe 'Issue Boards', :js do
click_card(card)
page.within('.issue-boards-sidebar') do
- expect(page).to have_content(issue2.title)
- expect(page).to have_content(issue2.to_reference)
- end
- end
-
- context 'milestone' do
- it 'adds a milestone' do
- click_card(card)
-
- page.within('.milestone') do
- click_link 'Edit'
-
- wait_for_requests
-
- click_link milestone.title
-
- wait_for_requests
-
- page.within('.value') do
- expect(page).to have_content(milestone.title)
- end
- end
- end
-
- it 'removes a milestone' do
- click_card(card)
-
- page.within('.milestone') do
- click_link 'Edit'
-
- wait_for_requests
-
- click_link "No milestone"
-
- wait_for_requests
-
- page.within('.value') do
- expect(page).not_to have_content(milestone.title)
- end
- end
- end
- end
-
- context 'time tracking' do
- let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['title'] }
-
- before do
- issue2.timelogs.create(time_spent: 14400, user: user)
- issue2.update!(time_estimate: 128800)
-
- click_card(card)
- end
-
- it 'shows time tracking progress bar' do
- expect(compare_meter_tooltip).to eq('Time remaining: 3d 7h 46m')
- end
-
- context 'when time_tracking_limit_to_hours is true' do
- let(:application_settings) { { time_tracking_limit_to_hours: true } }
-
- it 'shows time tracking progress bar' do
- expect(compare_meter_tooltip).to eq('Time remaining: 31h 46m')
- end
- end
- end
-
- context 'due date' do
- it 'updates due date' do
- click_card(card)
-
- page.within('.due_date') do
- click_link 'Edit'
-
- click_button Date.today.day.to_s
-
- wait_for_requests
-
- expect(page).to have_content(Date.today.to_s(:medium))
- end
- end
- end
-
- context 'subscription' do
- it 'changes issue subscription' do
- click_card(card)
- wait_for_requests
-
- page.within('.subscriptions') do
- find('[data-testid="subscription-toggle"] button:not(.is-checked)').click
- wait_for_requests
-
- expect(page).to have_css('[data-testid="subscription-toggle"] button.is-checked')
- end
- end
-
- it 'has checked subscription toggle when already subscribed' do
- create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true)
- visit project_board_path(project, board)
- wait_for_requests
-
- click_card(card)
- wait_for_requests
-
- page.within('.subscriptions') do
- find('[data-testid="subscription-toggle"] button.is-checked').click
- wait_for_requests
-
- expect(page).to have_css('[data-testid="subscription-toggle"] button:not(.is-checked)')
- end
+ expect(page).to have_content(issue.title)
+ expect(page).to have_content(issue.to_reference)
end
end
end
diff --git a/spec/features/boards/sidebar_subscription_spec.rb b/spec/features/boards/sidebar_subscription_spec.rb
new file mode 100644
index 00000000000..77766e909f9
--- /dev/null
+++ b/spec/features/boards/sidebar_subscription_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project issue boards sidebar subscription', :js do
+ include BoardHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue1) { create(:issue, project: project, relative_position: 1) }
+ let_it_be(:issue2) { create(:issue, project: project, relative_position: 2) }
+ let_it_be(:subscription) { create(:subscription, user: user, project: project, subscribable: issue2, subscribed: true) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:list) { create(:list, board: board, position: 0) }
+ let(:card1) { find('.board:nth-child(1) .board-card:nth-of-type(1)') }
+ let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
+
+ before do
+ project.add_maintainer(user)
+
+ sign_in(user)
+
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
+ context 'subscription' do
+ it 'changes issue subscription' do
+ click_card(card1)
+ wait_for_requests
+
+ page.within('.subscriptions') do
+ find('[data-testid="subscription-toggle"] button:not(.is-checked)').click
+ wait_for_requests
+
+ expect(page).to have_css('[data-testid="subscription-toggle"] button.is-checked')
+ end
+ end
+
+ it 'has checked subscription toggle when already subscribed' do
+ click_card(card2)
+ wait_for_requests
+
+ page.within('.subscriptions') do
+ find('[data-testid="subscription-toggle"] button.is-checked').click
+ wait_for_requests
+
+ expect(page).to have_css('[data-testid="subscription-toggle"] button:not(.is-checked)')
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/sidebar_time_tracking_spec.rb b/spec/features/boards/sidebar_time_tracking_spec.rb
new file mode 100644
index 00000000000..0cdf1e9a787
--- /dev/null
+++ b/spec/features/boards/sidebar_time_tracking_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project issue boards sidebar time tracking', :js do
+ include BoardHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:list) { create(:list, board: board, position: 0) }
+ let!(:issue) { create(:issue, project: project, relative_position: 1) }
+ let(:card) { find('.board:nth-child(1)').first('.board-card') }
+
+ let(:application_settings) { {} }
+
+ before do
+ project.add_maintainer(user)
+
+ sign_in(user)
+
+ stub_application_setting(application_settings)
+
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
+ context 'time tracking' do
+ let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['title'] }
+
+ before do
+ issue.timelogs.create!(time_spent: 14400, user: user)
+ issue.update!(time_estimate: 128800)
+
+ click_card(card)
+ end
+
+ it 'shows time tracking progress bar' do
+ expect(compare_meter_tooltip).to eq('Time remaining: 3d 7h 46m')
+ end
+
+ context 'when time_tracking_limit_to_hours is true' do
+ let(:application_settings) { { time_tracking_limit_to_hours: true } }
+
+ it 'shows time tracking progress bar' do
+ expect(compare_meter_tooltip).to eq('Time remaining: 31h 46m')
+ end
+ end
+ end
+end
diff --git a/spec/features/file_uploads/maven_package_spec.rb b/spec/features/file_uploads/maven_package_spec.rb
index e87eec58618..ab9f023bd8f 100644
--- a/spec/features/file_uploads/maven_package_spec.rb
+++ b/spec/features/file_uploads/maven_package_spec.rb
@@ -6,16 +6,17 @@ RSpec.describe 'Upload a maven package', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
- let(:api_path) { "/projects/#{project.id}/packages/maven/com/example/my-app/1.0/my-app-1.0-20180724.124855-1.jar" }
+ let(:project_id) { project.id }
+ let(:api_path) { "/projects/#{project_id}/packages/maven/com/example/my-app/1.0/my-app-1.0-20180724.124855-1.jar" }
let(:url) { capybara_url(api(api_path, personal_access_token: personal_access_token)) }
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
subject { HTTParty.put(url, body: file.read) }
- RSpec.shared_examples 'for a maven package' do
+ shared_examples 'for a maven package' do
it 'creates package files' do
expect { subject }
.to change { Packages::Package.maven.count }.by(1)
@@ -25,9 +26,9 @@ RSpec.describe 'Upload a maven package', :api, :js do
it { expect(subject.code).to eq(200) }
end
- RSpec.shared_examples 'for a maven sha1' do
+ shared_examples 'for a maven sha1' do
let(:dummy_package) { double(Packages::Package) }
- let(:api_path) { "/projects/#{project.id}/packages/maven/com/example/my-app/1.0/my-app-1.0-20180724.124855-1.jar.sha1" }
+ let(:api_path) { "/projects/#{project_id}/packages/maven/com/example/my-app/1.0/my-app-1.0-20180724.124855-1.jar.sha1" }
before do
# The sha verification done by the maven api is between:
@@ -42,8 +43,8 @@ RSpec.describe 'Upload a maven package', :api, :js do
it { expect(subject.code).to eq(204) }
end
- RSpec.shared_examples 'for a maven md5' do
- let(:api_path) { "/projects/#{project.id}/packages/maven/com/example/my-app/1.0/my-app-1.0-20180724.124855-1.jar.md5" }
+ shared_examples 'for a maven md5' do
+ let(:api_path) { "/projects/#{project_id}/packages/maven/com/example/my-app/1.0/my-app-1.0-20180724.124855-1.jar.md5" }
let(:file) { StringIO.new('dummy_package') }
it { expect(subject.code).to eq(200) }
@@ -52,4 +53,10 @@ RSpec.describe 'Upload a maven package', :api, :js do
it_behaves_like 'handling file uploads', 'for a maven package'
it_behaves_like 'handling file uploads', 'for a maven sha1'
it_behaves_like 'handling file uploads', 'for a maven md5'
+
+ context 'with an encoded project ID' do
+ let(:project_id) { "#{project.namespace.path}%2F#{project.path}" }
+
+ it_behaves_like 'handling file uploads', 'for a maven package'
+ end
end
diff --git a/spec/features/file_uploads/nuget_package_spec.rb b/spec/features/file_uploads/nuget_package_spec.rb
index 6e05e5d1a6e..871c0274445 100644
--- a/spec/features/file_uploads/nuget_package_spec.rb
+++ b/spec/features/file_uploads/nuget_package_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Upload a nuget package', :api, :js do
include_context 'file upload requests helpers'
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let(:api_path) { "/projects/#{project.id}/packages/nuget/" }
@@ -21,7 +21,7 @@ RSpec.describe 'Upload a nuget package', :api, :js do
)
end
- RSpec.shared_examples 'for a nuget package' do
+ shared_examples 'for a nuget package' do
it 'creates package files' do
expect { subject }
.to change { Packages::Package.nuget.count }.by(1)
diff --git a/spec/features/file_uploads/rubygem_package_spec.rb b/spec/features/file_uploads/rubygem_package_spec.rb
new file mode 100644
index 00000000000..4a5891fdfed
--- /dev/null
+++ b/spec/features/file_uploads/rubygem_package_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Upload a RubyGems package', :api, :js do
+ include_context 'file upload requests helpers'
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.owner }
+ let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ let(:api_path) { "/projects/#{project_id}/packages/rubygems/api/v1/gems" }
+ let(:url) { capybara_url(api(api_path)) }
+ let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
+
+ subject do
+ HTTParty.post(
+ url,
+ headers: { 'Authorization' => personal_access_token.token },
+ body: { file: file }
+ )
+ end
+
+ shared_examples 'for a Rubygems package' do
+ it 'creates package files' do
+ expect { subject }
+ .to change { Packages::Package.rubygems.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+ end
+
+ it { expect(subject.code).to eq(201) }
+ end
+
+ context 'with an integer project ID' do
+ let(:project_id) { project.id }
+
+ it_behaves_like 'handling file uploads', 'for a Rubygems package'
+ end
+
+ context 'with an encoded project ID' do
+ let(:project_id) { "#{project.namespace.path}%2F#{project.path}" }
+
+ it_behaves_like 'handling file uploads', 'for a Rubygems package'
+ end
+end
diff --git a/spec/features/groups/settings/user_searches_in_settings_spec.rb b/spec/features/groups/settings/user_searches_in_settings_spec.rb
index 7843e610548..fa2f91dec7d 100644
--- a/spec/features/groups/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/groups/settings/user_searches_in_settings_spec.rb
@@ -11,10 +11,12 @@ RSpec.describe 'User searches group settings', :js do
sign_in(user)
end
- context 'in General settings page' do
- let(:visit_path) { edit_group_path(group) }
+ context 'in general settings page' do
+ before do
+ visit edit_group_path(group)
+ end
- it_behaves_like 'can search settings with feature flag check', 'Naming', 'Permissions'
+ it_behaves_like 'can search settings', 'Naming', 'Permissions'
end
context 'in Integrations page' do
diff --git a/spec/features/profiles/user_search_settings_spec.rb b/spec/features/profiles/user_search_settings_spec.rb
index 60df0d7532b..64a8556e349 100644
--- a/spec/features/profiles/user_search_settings_spec.rb
+++ b/spec/features/profiles/user_search_settings_spec.rb
@@ -10,9 +10,11 @@ RSpec.describe 'User searches their settings', :js do
end
context 'in profile page' do
- let(:visit_path) { profile_path }
+ before do
+ visit profile_path
+ end
- it_behaves_like 'can search settings with feature flag check', 'Public Avatar', 'Main settings'
+ it_behaves_like 'can search settings', 'Public Avatar', 'Main settings'
end
context 'in preferences page' do
diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb
index 2dba5942f3e..31b61781f45 100644
--- a/spec/features/projects/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb
@@ -11,9 +11,11 @@ RSpec.describe 'User searches project settings', :js do
end
context 'in general settings page' do
- let(:visit_path) { edit_project_path(project) }
+ before do
+ visit edit_project_path(project)
+ end
- it_behaves_like 'can search settings with feature flag check', 'Naming', 'Visibility'
+ it_behaves_like 'can search settings', 'Naming', 'Visibility'
end
context 'in Integrations page' do
diff --git a/spec/fixtures/api/schemas/entities/member.json b/spec/fixtures/api/schemas/entities/member.json
index 03b1872632e..f06687f9809 100644
--- a/spec/fixtures/api/schemas/entities/member.json
+++ b/spec/fixtures/api/schemas/entities/member.json
@@ -8,6 +8,7 @@
"requested_at",
"source",
"valid_roles",
+ "type",
"can_update",
"can_remove",
"is_direct_member"
@@ -40,6 +41,7 @@
"additionalProperties": false
},
"valid_roles": { "type": "object" },
+ "type": { "type": "string" },
"created_by": {
"type": "object",
"required": ["name", "web_url"],
diff --git a/spec/frontend/__helpers__/mock_apollo_helper.js b/spec/frontend/__helpers__/mock_apollo_helper.js
index 914cce1d662..bd97a06071a 100644
--- a/spec/frontend/__helpers__/mock_apollo_helper.js
+++ b/spec/frontend/__helpers__/mock_apollo_helper.js
@@ -2,11 +2,15 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { createMockClient } from 'mock-apollo-client';
import VueApollo from 'vue-apollo';
-export default (handlers = [], resolvers = {}) => {
- const fragmentMatcher = { match: () => true };
+const defaultCacheOptions = {
+ fragmentMatcher: { match: () => true },
+ addTypename: false,
+};
+
+export default (handlers = [], resolvers = {}, cacheOptions = {}) => {
const cache = new InMemoryCache({
- fragmentMatcher,
- addTypename: false,
+ ...defaultCacheOptions,
+ ...cacheOptions,
});
const mockClient = createMockClient({ cache, resolvers });
diff --git a/spec/frontend/captcha/apollo_captcha_link_spec.js b/spec/frontend/captcha/apollo_captcha_link_spec.js
new file mode 100644
index 00000000000..b4863d7bc19
--- /dev/null
+++ b/spec/frontend/captcha/apollo_captcha_link_spec.js
@@ -0,0 +1,165 @@
+import { ApolloLink, Observable } from 'apollo-link';
+
+import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
+import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
+import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
+
+jest.mock('~/captcha/wait_for_captcha_to_be_solved');
+
+describe('apolloCaptchaLink', () => {
+ const SPAM_LOG_ID = 'SPAM_LOG_ID';
+ const CAPTCHA_SITE_KEY = 'CAPTCHA_SITE_KEY';
+ const CAPTCHA_RESPONSE = 'CAPTCHA_RESPONSE';
+
+ const SUCCESS_RESPONSE = {
+ data: {
+ user: {
+ id: 3,
+ name: 'foo',
+ },
+ },
+ errors: [],
+ };
+
+ const NON_CAPTCHA_ERROR_RESPONSE = {
+ data: {
+ user: null,
+ },
+ errors: [
+ {
+ message: 'Something is severely wrong with your query.',
+ path: ['user'],
+ locations: [{ line: 2, column: 3 }],
+ extensions: {
+ message: 'Object not found',
+ type: 2,
+ },
+ },
+ ],
+ };
+
+ const SPAM_ERROR_RESPONSE = {
+ data: {
+ user: null,
+ },
+ errors: [
+ {
+ message: 'Your Query was detected to be SPAM.',
+ path: ['user'],
+ locations: [{ line: 2, column: 3 }],
+ extensions: {
+ spam: true,
+ },
+ },
+ ],
+ };
+
+ const CAPTCHA_ERROR_RESPONSE = {
+ data: {
+ user: null,
+ },
+ errors: [
+ {
+ message: 'This is an unrelated error, captcha should still work despite this.',
+ path: ['user'],
+ locations: [{ line: 2, column: 3 }],
+ },
+ {
+ message: 'You need to solve a Captcha.',
+ path: ['user'],
+ locations: [{ line: 2, column: 3 }],
+ extensions: {
+ spam: true,
+ needs_captcha_response: true,
+ captcha_site_key: CAPTCHA_SITE_KEY,
+ spam_log_id: SPAM_LOG_ID,
+ },
+ },
+ ],
+ };
+
+ let link;
+
+ let mockLinkImplementation;
+ let mockContext;
+
+ const setupLink = (...responses) => {
+ mockLinkImplementation = jest.fn().mockImplementation(() => {
+ return Observable.of(responses.shift());
+ });
+ link = ApolloLink.from([apolloCaptchaLink, new ApolloLink(mockLinkImplementation)]);
+ };
+
+ function mockOperation() {
+ mockContext = jest.fn();
+ return { operationName: 'operation', variables: {}, setContext: mockContext };
+ }
+
+ it('successful responses are passed through', (done) => {
+ setupLink(SUCCESS_RESPONSE);
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(SUCCESS_RESPONSE);
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('non-spam related errors are passed through', (done) => {
+ setupLink(NON_CAPTCHA_ERROR_RESPONSE);
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(NON_CAPTCHA_ERROR_RESPONSE);
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ expect(mockContext).not.toHaveBeenCalled();
+ expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('unresolvable SPAM errors are passed through', (done) => {
+ setupLink(SPAM_ERROR_RESPONSE);
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(SPAM_ERROR_RESPONSE);
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ expect(mockContext).not.toHaveBeenCalled();
+ expect(waitForCaptchaToBeSolved).not.toHaveBeenCalled();
+ done();
+ });
+ });
+
+ describe('resolvable SPAM errors', () => {
+ it('re-submits request with SPAM headers if the captcha modal was solved correctly', (done) => {
+ waitForCaptchaToBeSolved.mockResolvedValue(CAPTCHA_RESPONSE);
+ setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
+ link.request(mockOperation()).subscribe((result) => {
+ expect(result).toEqual(SUCCESS_RESPONSE);
+ expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
+ expect(mockContext).toHaveBeenCalledWith({
+ headers: {
+ 'X-GitLab-Captcha-Response': CAPTCHA_RESPONSE,
+ 'X-GitLab-Spam-Log-Id': SPAM_LOG_ID,
+ },
+ });
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(2);
+ done();
+ });
+ });
+
+ it('throws error if the captcha modal was not solved correctly', (done) => {
+ const error = new UnsolvedCaptchaError();
+ waitForCaptchaToBeSolved.mockRejectedValue(error);
+
+ setupLink(CAPTCHA_ERROR_RESPONSE, SUCCESS_RESPONSE);
+ link.request(mockOperation()).subscribe({
+ next: done.catch,
+ error: (result) => {
+ expect(result).toEqual(error);
+ expect(waitForCaptchaToBeSolved).toHaveBeenCalledWith(CAPTCHA_SITE_KEY);
+ expect(mockContext).not.toHaveBeenCalled();
+ expect(mockLinkImplementation).toHaveBeenCalledTimes(1);
+ done();
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index fe63f9bfaa7..55866f90baa 100644
--- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -39,6 +39,7 @@ describe('InviteActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toEqual({
memberId: member.id,
+ memberType: null,
message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`,
title: 'Revoke invite',
isAccessRequest: false,
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index 437b3e705a4..0d66f343fda 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -24,6 +24,7 @@ describe('RemoveMemberButton', () => {
store: createStore(state),
propsData: {
memberId: 1,
+ memberType: 'GroupMember',
message: 'Are you sure you want to remove John Smith?',
title: 'Remove member',
isAccessRequest: true,
@@ -44,6 +45,7 @@ describe('RemoveMemberButton', () => {
expect(wrapper.attributes()).toMatchObject({
'data-member-path': '/groups/foo-bar/-/group_members/1',
+ 'data-member-type': 'GroupMember',
'data-message': 'Are you sure you want to remove John Smith?',
'data-is-access-request': 'true',
'aria-label': 'Remove member',
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
index 1d7ea5b3109..f43779b8970 100644
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
@@ -39,6 +39,7 @@ describe('UserActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toEqual({
memberId: member.id,
+ memberType: 'GroupMember',
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"`,
title: 'Remove member',
isAccessRequest: false,
@@ -86,4 +87,40 @@ describe('UserActionButtons', () => {
expect(findRemoveMemberButton().exists()).toBe(false);
});
});
+
+ describe('when group member', () => {
+ beforeEach(() => {
+ createComponent({
+ member: {
+ ...member,
+ type: 'GroupMember',
+ },
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('sets member type correctly', () => {
+ expect(findRemoveMemberButton().props().memberType).toBe('GroupMember');
+ });
+ });
+
+ describe('when project member', () => {
+ beforeEach(() => {
+ createComponent({
+ member: {
+ ...member,
+ type: 'ProjectMember',
+ },
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('sets member type correctly', () => {
+ expect(findRemoveMemberButton().props().memberType).toBe('ProjectMember');
+ });
+ });
});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 6a73b2fcf8c..5a2c6d560c0 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -11,6 +11,7 @@ export const member = {
fullName: 'Foo Bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
+ type: 'GroupMember',
user: {
id: 123,
name: 'Administrator',
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 44d8e467f51..adb7eda9587 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
+import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import { mockPipelineResponse } from './mock_data';
const defaultProvide = {
@@ -22,15 +23,19 @@ describe('Pipeline graph wrapper', () => {
const getAlert = () => wrapper.find(GlAlert);
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const getGraph = () => wrapper.find(PipelineGraph);
+ const getViewSelector = () => wrapper.find(GraphViewSelector);
const createComponent = ({
apolloProvider,
data = {},
- provide = defaultProvide,
+ provide = {},
mountFn = shallowMount,
} = {}) => {
wrapper = mountFn(PipelineGraphWrapper, {
- provide,
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
apolloProvider,
data() {
return {
@@ -40,13 +45,14 @@ describe('Pipeline graph wrapper', () => {
});
};
- const createComponentWithApollo = (
+ const createComponentWithApollo = ({
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
- ) => {
+ provide = {},
+ } = {}) => {
const requestHandlers = [[getPipelineDetails, getPipelineDetailsHandler]];
const apolloProvider = createMockApollo(requestHandlers);
- createComponent({ apolloProvider });
+ createComponent({ apolloProvider, provide });
};
afterEach(() => {
@@ -100,7 +106,9 @@ describe('Pipeline graph wrapper', () => {
describe('when there is an error', () => {
beforeEach(async () => {
- createComponentWithApollo(jest.fn().mockRejectedValue(new Error('GraphQL error')));
+ createComponentWithApollo({
+ getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
+ });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
});
@@ -154,7 +162,7 @@ describe('Pipeline graph wrapper', () => {
.mockResolvedValueOnce(mockPipelineResponse)
.mockResolvedValueOnce(errorData);
- createComponentWithApollo(failSucceedFail);
+ createComponentWithApollo({ getPipelineDetailsHandler: failSucceedFail });
await wrapper.vm.$nextTick();
});
@@ -174,4 +182,37 @@ describe('Pipeline graph wrapper', () => {
expect(getGraph().exists()).toBe(true);
});
});
+
+ describe('view dropdown', () => {
+ describe('when feature flag is off', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('does not appear', () => {
+ expect(getViewSelector().exists()).toBe(false);
+ });
+ });
+
+ describe('when feature flag is on', () => {
+ beforeEach(async () => {
+ createComponentWithApollo({
+ provide: {
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('appears', () => {
+ expect(getViewSelector().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
index 78fe6d53eee..1ef3cc348bd 100644
--- a/spec/frontend/vue_shared/components/remove_member_modal_spec.js
+++ b/spec/frontend/vue_shared/components/remove_member_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlFormCheckbox, GlModal } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
@@ -15,12 +15,20 @@ describe('RemoveMemberModal', () => {
});
describe.each`
- state | isAccessRequest | actionText | checkboxTestDescription | checkboxExpected | message
- ${'removing a member'} | ${'false'} | ${'Remove member'} | ${'shows a checkbox to allow removal from related issues and MRs'} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
- ${'denying an access request'} | ${'true'} | ${'Deny access request'} | ${'does not show a checkbox'} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"}
+ state | memberType | isAccessRequest | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message
+ ${'removing a group member'} | ${'GroupMember'} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
+ ${'removing a project member'} | ${'ProjectMember'} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
+ ${'denying an access request'} | ${'ProjectMember'} | ${'true'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"}
`(
'when $state',
- ({ actionText, isAccessRequest, message, checkboxTestDescription, checkboxExpected }) => {
+ ({
+ actionText,
+ memberType,
+ isAccessRequest,
+ message,
+ removeSubMembershipsCheckboxExpected,
+ unassignIssuablesCheckboxExpected,
+ }) => {
beforeEach(() => {
wrapper = shallowMount(RemoveMemberModal, {
data() {
@@ -29,6 +37,7 @@ describe('RemoveMemberModal', () => {
isAccessRequest,
message,
memberPath,
+ memberType,
},
};
},
@@ -47,8 +56,20 @@ describe('RemoveMemberModal', () => {
expect(wrapper.find('[data-testid=modal-message]').text()).toBe(message);
});
- it(`${checkboxTestDescription}`, () => {
- expect(wrapper.find(GlFormCheckbox).exists()).toBe(checkboxExpected);
+ it(`shows ${
+ removeSubMembershipsCheckboxExpected ? 'a' : 'no'
+ } checkbox to remove direct memberships of subgroups/projects`, () => {
+ expect(wrapper.find('[name=remove_sub_memberships]').exists()).toBe(
+ removeSubMembershipsCheckboxExpected,
+ );
+ });
+
+ it(`shows ${
+ unassignIssuablesCheckboxExpected ? 'a' : 'no'
+ } checkbox to allow removal from related issues and MRs`, () => {
+ expect(wrapper.find('[name=unassign_issuables]').exists()).toBe(
+ unassignIssuablesCheckboxExpected,
+ );
});
it('submits the form when the modal is submitted', () => {
diff --git a/spec/lib/api/helpers/authentication_spec.rb b/spec/lib/api/helpers/authentication_spec.rb
index 461b0d2f6f9..eea5c10d4f8 100644
--- a/spec/lib/api/helpers/authentication_spec.rb
+++ b/spec/lib/api/helpers/authentication_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe API::Helpers::Authentication do
let_it_be(:project, reload: true) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+ let_it_be(:ci_build) { create(:ci_build, :running, user: user) }
describe 'class methods' do
subject { Class.new.include(described_class::ClassMethods).new }
@@ -176,6 +177,20 @@ RSpec.describe API::Helpers::Authentication do
end
end
+ describe '#ci_build_from_namespace_inheritable' do
+ subject { object.ci_build_from_namespace_inheritable }
+
+ it 'returns #token_from_namespace_inheritable if it is a ci build' do
+ expect(object).to receive(:token_from_namespace_inheritable).and_return(ci_build)
+ expect(subject).to be(ci_build)
+ end
+
+ it 'returns nil if #token_from_namespace_inheritable is not a ci build' do
+ expect(object).to receive(:token_from_namespace_inheritable).and_return(personal_access_token)
+ expect(subject).to eq(nil)
+ end
+ end
+
describe '#user_from_namespace_inheritable' do
subject { object.user_from_namespace_inheritable }
diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
index 03a6cc34962..f7cb6b92b48 100644
--- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
@@ -139,6 +139,12 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do
end
context "structure" do
+ it 'starts with two newlines to separate with actual commit message' do
+ doc = filter(commit_message_html)
+
+ expect(doc.xpath('pre').text).to start_with("\n\n")
+ end
+
it 'preserves the commit trailer structure' do
doc = filter(commit_message_html)
diff --git a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
index 67ffdee0c4a..69068883096 100644
--- a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::OAuth::AuthHash do
- let(:provider) { 'ldap'.freeze }
+ let(:provider) { 'ldap' }
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
index e55281f9705..21d636aa7f0 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
@@ -42,6 +42,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
end
let(:save_incompleted) { true }
+ let(:dot_com) { true }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, current_user: user, yaml_processor_result: yaml_processor_result, save_incompleted: save_incompleted
@@ -51,9 +52,77 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
describe '#perform!' do
subject(:perform!) { step.perform! }
- context 'when validation returns true' do
+ let(:validation_service_url) { 'https://validation-service.external/' }
+
+ before do
+ stub_env('EXTERNAL_VALIDATION_SERVICE_URL', validation_service_url)
+ allow(Gitlab).to receive(:com?).and_return(dot_com)
+ end
+
+ shared_examples 'successful external authorization' do
+ it 'does not drop the pipeline' do
+ perform!
+
+ expect(pipeline.status).not_to eq('failed')
+ expect(pipeline.errors).to be_empty
+ end
+
+ it 'does not break the chain' do
+ perform!
+
+ expect(step.break?).to be false
+ end
+
+ it 'logs the authorization' do
+ expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline authorized', project_id: project.id, user_id: user.id)
+
+ perform!
+ end
+ end
+
+ context 'when validation returns 200 OK' do
+ before do
+ stub_request(:post, validation_service_url).to_return(status: 200, body: "{}")
+ end
+
+ it_behaves_like 'successful external authorization'
+ end
+
+ context 'when validation returns 404 Not Found' do
+ before do
+ stub_request(:post, validation_service_url).to_return(status: 404, body: "{}")
+ end
+
+ it_behaves_like 'successful external authorization'
+ end
+
+ context 'when validation returns 500 Internal Server Error' do
+ before do
+ stub_request(:post, validation_service_url).to_return(status: 500, body: "{}")
+ end
+
+ it_behaves_like 'successful external authorization'
+ end
+
+ context 'when validation raises exceptions' do
+ before do
+ stub_request(:post, validation_service_url).to_raise(Net::OpenTimeout)
+ end
+
+ it_behaves_like 'successful external authorization'
+
+ it 'logs exceptions' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(instance_of(Net::OpenTimeout), { project_id: project.id })
+
+ perform!
+ end
+ end
+
+ context 'when the feature flag is disabled' do
before do
- allow(step).to receive(:validate_external).and_return(true)
+ stub_feature_flags(ci_external_validation_service: false)
+ stub_request(:post, validation_service_url)
end
it 'does not drop the pipeline' do
@@ -69,16 +138,45 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
expect(step.break?).to be false
end
+ it 'does not make requests' do
+ perform!
+
+ expect(WebMock).not_to have_requested(:post, validation_service_url)
+ end
+ end
+
+ context 'when not on .com' do
+ let(:dot_com) { false }
+
+ before do
+ stub_feature_flags(ci_external_validation_service: false)
+ stub_request(:post, validation_service_url).to_return(status: 404, body: "{}")
+ end
+
+ it 'drops the pipeline' do
+ perform!
+
+ expect(pipeline.status).to eq('failed')
+ expect(pipeline).to be_persisted
+ expect(pipeline.errors.to_a).to include('External validation failed')
+ end
+
+ it 'breaks the chain' do
+ perform!
+
+ expect(step.break?).to be true
+ end
+
it 'logs the authorization' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline authorized', project_id: project.id, user_id: user.id)
+ expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline not authorized', project_id: project.id, user_id: user.id)
perform!
end
end
- context 'when validation return false' do
+ context 'when validation returns 406 Not Acceptable' do
before do
- allow(step).to receive(:validate_external).and_return(false)
+ stub_request(:post, validation_service_url).to_return(status: 406, body: "{}")
end
it 'drops the pipeline' do
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 597e4ca9b03..532127dcb20 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -63,8 +63,14 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_defa
describe '#update_interval' do
context 'it is not being watched' do
- it 'returns 30 seconds' do
- expect(trace.update_interval).to eq(30.seconds)
+ it { expect(trace.update_interval).to eq(60.seconds) }
+
+ context 'when feature flag ci_lower_frequency_trace_update is disabled' do
+ before do
+ stub_feature_flags(ci_lower_frequency_trace_update: false)
+ end
+
+ it { expect(trace.update_interval).to eq(30.seconds) }
end
end
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
index 96e6e485841..7c57d162e9b 100644
--- a/spec/lib/gitlab/http_connection_adapter_spec.rb
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -124,130 +124,5 @@ RSpec.describe Gitlab::HTTPConnectionAdapter do
expect(connection.port).to eq(443)
end
end
-
- context 'when proxy settings are configured' do
- let(:options) do
- {
- http_proxyaddr: 'https://proxy.org',
- http_proxyport: 1557,
- http_proxyuser: 'user',
- http_proxypass: 'pass'
- }
- end
-
- before do
- stub_all_dns('https://proxy.org', ip_address: '166.84.12.54')
- end
-
- it 'sets up the proxy settings' do
- expect(connection.proxy_address).to eq('https://166.84.12.54')
- expect(connection.proxy_port).to eq(1557)
- expect(connection.proxy_user).to eq('user')
- expect(connection.proxy_pass).to eq('pass')
- end
-
- context 'when the address has path' do
- before do
- options[:http_proxyaddr] = 'https://proxy.org/path'
- end
-
- it 'sets up the proxy settings' do
- expect(connection.proxy_address).to eq('https://166.84.12.54/path')
- expect(connection.proxy_port).to eq(1557)
- end
- end
-
- context 'when the port is in the address and port' do
- before do
- options[:http_proxyaddr] = 'https://proxy.org:1422'
- end
-
- it 'sets up the proxy settings' do
- expect(connection.proxy_address).to eq('https://166.84.12.54')
- expect(connection.proxy_port).to eq(1557)
- end
-
- context 'when the port is only in the address' do
- before do
- options[:http_proxyport] = nil
- end
-
- it 'sets up the proxy settings' do
- expect(connection.proxy_address).to eq('https://166.84.12.54')
- expect(connection.proxy_port).to eq(1422)
- end
- end
- end
-
- context 'when it is a request to local network' do
- before do
- options[:http_proxyaddr] = 'http://172.16.0.0/12'
- end
-
- it 'raises error' do
- expect { subject }.to raise_error(
- Gitlab::HTTP::BlockedUrlError,
- "URL 'http://172.16.0.0:1557/12' is blocked: Requests to the local network are not allowed"
- )
- end
-
- context 'when local request allowed' do
- before do
- options[:allow_local_requests] = true
- end
-
- it 'sets up the connection' do
- expect(connection.proxy_address).to eq('http://172.16.0.0/12')
- expect(connection.proxy_port).to eq(1557)
- end
- end
- end
-
- context 'when it is a request to local address' do
- before do
- options[:http_proxyaddr] = 'http://127.0.0.1'
- end
-
- it 'raises error' do
- expect { subject }.to raise_error(
- Gitlab::HTTP::BlockedUrlError,
- "URL 'http://127.0.0.1:1557' is blocked: Requests to localhost are not allowed"
- )
- end
-
- context 'when local request allowed' do
- before do
- options[:allow_local_requests] = true
- end
-
- it 'sets up the connection' do
- expect(connection.proxy_address).to eq('http://127.0.0.1')
- expect(connection.proxy_port).to eq(1557)
- end
- end
- end
-
- context 'when http(s) environment variable is set' do
- before do
- stub_env('https_proxy' => 'https://my.proxy')
- end
-
- it 'sets up the connection' do
- expect(connection.proxy_address).to eq('https://proxy.org')
- expect(connection.proxy_port).to eq(1557)
- end
- end
-
- context 'when DNS rebinding protection is disabled' do
- before do
- stub_application_setting(dns_rebinding_protection_enabled: false)
- end
-
- it 'sets up the connection' do
- expect(connection.proxy_address).to eq('https://proxy.org')
- expect(connection.proxy_port).to eq(1557)
- end
- end
- end
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index d12dcdae955..70e4410649b 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -93,7 +93,25 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
describe '.track_event' do
- context 'with feature flag set' do
+ context 'with redis_hll_tracking' do
+ it 'tracks the event when feature enabled' do
+ stub_feature_flags(redis_hll_tracking: true)
+
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+
+ it 'does not track the event with feature flag disabled' do
+ stub_feature_flags(redis_hll_tracking: false)
+
+ expect(Gitlab::Redis::HLL).not_to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+ end
+
+ context 'with event feature flag set' do
it 'tracks the event when feature enabled' do
stub_feature_flags(feature => true)
@@ -111,7 +129,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
end
- context 'with no feature flag set' do
+ context 'with no event feature flag set' do
it 'tracks the event' do
expect(Gitlab::Redis::HLL).to receive(:add)
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 5582ff25b04..24aeb1e495a 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1361,21 +1361,33 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
%w[source_code ci_secrets_management incident_management_alerts snippets terraform]
end
- it 'has all known_events' do
- expect(subject).to have_key(:redis_hll_counters)
+ context 'with redis_hll_tracking feature enabled' do
+ it 'has all known_events' do
+ stub_feature_flags(redis_hll_tracking: true)
- expect(subject[:redis_hll_counters].keys).to match_array(categories)
+ expect(subject).to have_key(:redis_hll_counters)
- categories.each do |category|
- keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category)
+ expect(subject[:redis_hll_counters].keys).to match_array(categories)
- metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" }
+ categories.each do |category|
+ keys = ::Gitlab::UsageDataCounters::HLLRedisCounter.events_for_category(category)
- if ineligible_total_categories.exclude?(category)
- metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly")
+ metrics = keys.map { |key| "#{key}_weekly" } + keys.map { |key| "#{key}_monthly" }
+
+ if ineligible_total_categories.exclude?(category)
+ metrics.append("#{category}_total_unique_counts_weekly", "#{category}_total_unique_counts_monthly")
+ end
+
+ expect(subject[:redis_hll_counters][category].keys).to match_array(metrics)
end
+ end
+ end
+
+ context 'with redis_hll_tracking disabled' do
+ it 'does not have redis_hll_tracking key' do
+ stub_feature_flags(redis_hll_tracking: false)
- expect(subject[:redis_hll_counters][category].keys).to match_array(metrics)
+ expect(subject).not_to have_key(:redis_hll_counters)
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index f92510bddbf..8c563352725 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -911,24 +911,10 @@ RSpec.describe Namespace do
it { expect(namespace.all_projects.to_a).to match_array([project2, project1]) }
it { expect(child.all_projects.to_a).to match_array([project2]) }
- context 'when recursive_namespace_lookup_as_inner_join feature flag is on' do
- before do
- stub_feature_flags(recursive_namespace_lookup_as_inner_join: true)
- end
-
- it 'queries for the namespace and its descendants' do
- expect(namespace.all_projects).to match_array([project1, project2])
- end
- end
+ it 'queries for the namespace and its descendants' do
+ expect(Project).to receive(:where).with(namespace: [namespace, child])
- context 'when recursive_namespace_lookup_as_inner_join feature flag is off' do
- before do
- stub_feature_flags(recursive_namespace_lookup_as_inner_join: false)
- end
-
- it 'queries for the namespace and its descendants' do
- expect(namespace.all_projects).to match_array([project1, project2])
- end
+ namespace.all_projects
end
end
diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
index 7a679a03b53..1dc714e1320 100644
--- a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
+++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
@@ -3,17 +3,70 @@
require 'spec_helper'
RSpec.describe Projects::Settings::DeployKeysPresenter do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let_it_be(:project, refind: true) { create(:project) }
+ let_it_be(:other_project) { create(:project) }
+ let_it_be(:user) { create(:user) }
subject(:presenter) do
described_class.new(project, current_user: user)
end
+ before_all do
+ project.add_maintainer(user)
+ other_project.add_maintainer(user)
+ end
+
it 'inherits from Gitlab::View::Presenter::Simple' do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Simple)
end
+ describe 'deploy key groups' do
+ let_it_be(:deploy_key) { create(:deploy_key, projects: [project]) }
+ let_it_be(:other_deploy_key) { create(:deploy_key, projects: [other_project]) }
+ let_it_be(:public_deploy_key) { create(:deploy_key, public: true) }
+ let_it_be(:unrelated_project) { create(:project, :private) }
+ let_it_be(:unrelated_deploy_key) { create(:deploy_key, projects: [unrelated_project]) }
+
+ shared_examples_for 'correct behavior' do
+ context 'with enabled keys' do
+ it 'returns correct deploy keys' do
+ expect(presenter.enabled_keys).to eq([deploy_key])
+ expect(presenter.enabled_keys_size).to eq(1)
+ end
+ end
+
+ context 'with available keys' do
+ it 'returns correct deploy keys' do
+ expect(presenter.available_keys).to eq([other_deploy_key, public_deploy_key])
+ end
+ end
+
+ context 'with available project keys' do
+ it 'returns correct deploy keys' do
+ expect(presenter.available_project_keys).to eq([other_deploy_key])
+ expect(presenter.available_project_keys_size).to eq(1)
+ end
+ end
+
+ context 'with available public keys' do
+ it 'returns correct deploy keys' do
+ expect(presenter.available_public_keys).to eq([public_deploy_key])
+ expect(presenter.available_public_keys_size).to eq(1)
+ end
+ end
+ end
+
+ it_behaves_like 'correct behavior'
+
+ context 'when optimize_deploy_keys_presenter feature flag is disabled' do
+ before do
+ stub_feature_flags(optimize_deploy_keys_presenter: false)
+ end
+
+ it_behaves_like 'correct behavior'
+ end
+ end
+
describe '#enabled_keys' do
let!(:deploy_key) { create(:deploy_key, public: true) }
diff --git a/spec/requests/api/ci/runner/jobs_trace_spec.rb b/spec/requests/api/ci/runner/jobs_trace_spec.rb
index d566c3b5032..5d508a2ac0d 100644
--- a/spec/requests/api/ci/runner/jobs_trace_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_trace_spec.rb
@@ -210,11 +210,24 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when build trace is not being watched' do
- it 'returns X-GitLab-Trace-Update-Interval as 30' do
+ it 'returns the interval in X-GitLab-Trace-Update-Interval' do
patch_the_trace
expect(response).to have_gitlab_http_status(:accepted)
- expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30')
+ expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('60')
+ end
+
+ context 'when ci_lower_frequency_trace_update feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_lower_frequency_trace_update: false)
+ end
+
+ it 'returns the legacy interval in X-GitLab-Trace-Update-Interval' do
+ patch_the_trace
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30')
+ end
end
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 919c8d29406..2762048225a 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -555,6 +555,34 @@ RSpec.describe API::Members do
end
end
+ describe 'DELETE /groups/:id/members/:user_id' do
+ let(:other_user) { create(:user) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ before do
+ nested_group.add_developer(developer)
+ nested_group.add_developer(other_user)
+ end
+
+ it 'deletes only the member with skip_subresources=true' do
+ expect do
+ delete api("/groups/#{group.id}/members/#{developer.id}", maintainer), params: { skip_subresources: true }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end.to change { group.members.count }.by(-1)
+ .and change { nested_group.members.count }.by(0)
+ end
+
+ it 'deletes member and its sub memberships with skip_subresources=false' do
+ expect do
+ delete api("/groups/#{group.id}/members/#{developer.id}", maintainer), params: { skip_subresources: false }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end.to change { group.members.count }.by(-1)
+ .and change { nested_group.members.count }.by(-1)
+ end
+ end
+
[false, true].each do |all|
it_behaves_like 'GET /:source_type/:id/members/(all)', 'project', all do
let(:source) { project }
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
index 0277aa73220..54fe0b985df 100644
--- a/spec/requests/api/nuget_project_packages_spec.rb
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -188,6 +188,10 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'deploy token for package uploads'
+ it_behaves_like 'job token for package uploads', authorize_endpoint: true do
+ let_it_be(:job) { create(:ci_build, :running, user: user) }
+ end
+
it_behaves_like 'rejects nuget access with unknown target id'
it_behaves_like 'rejects nuget access with invalid target id'
@@ -251,6 +255,10 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'deploy token for package uploads'
+ it_behaves_like 'job token for package uploads' do
+ let_it_be(:job) { create(:ci_build, :running, user: user) }
+ end
+
it_behaves_like 'rejects nuget access with unknown target id'
it_behaves_like 'rejects nuget access with invalid target id'
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index ae5b132f409..718004a0087 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'deploy token for package uploads'
- it_behaves_like 'job token for package uploads'
+ it_behaves_like 'job token for package uploads', authorize_endpoint: true
it_behaves_like 'rejects PyPI access with unknown project id'
end
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index 42a92504839..b54fc45d36a 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -145,28 +145,5 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do
expect { subject }.to change { build.status }.to(after_status)
end
end
-
- context 'when FF skip_dag_manual_and_delayed_jobs is disabled on the project' do
- let_it_be(:other_project) { create(:project) }
-
- before do
- stub_feature_flags(skip_dag_manual_and_delayed_jobs: other_project)
- end
-
- where(:build_when, :current_status, :after_status) do
- :on_success | 'success' | 'pending'
- :on_success | 'skipped' | 'skipped'
- :manual | 'success' | 'manual'
- :manual | 'skipped' | 'manual'
- :delayed | 'success' | 'manual'
- :delayed | 'skipped' | 'manual'
- end
-
- with_them do
- it 'proceeds the build' do
- expect { subject }.to change { build.status }.to(after_status)
- end
- end
- end
end
end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 4f731ad5852..b9f382d3cd8 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -289,60 +289,137 @@ RSpec.describe Members::DestroyService do
let(:group_project) { create(:project, :public, group: group) }
let(:control_project) { create(:project, group: subsubgroup) }
- before do
- create(:group_member, :developer, group: subsubgroup, user: member_user)
- create(:project_member, :invited, project: group_project, created_by: member_user)
- create(:group_member, :invited, group: group, created_by: member_user)
- create(:project_member, :invited, project: subsubproject, created_by: member_user)
- create(:group_member, :invited, group: subgroup, created_by: member_user)
+ context 'with memberships' do
+ before do
+ subgroup.add_developer(member_user)
+ subsubgroup.add_developer(member_user)
+ subsubproject.add_developer(member_user)
+ group_project.add_developer(member_user)
+ control_project.add_maintainer(user)
+ group.add_owner(user)
+
+ @group_member = create(:group_member, :developer, group: group, user: member_user)
+ end
- subsubproject.add_developer(member_user)
- control_project.add_maintainer(user)
- group.add_owner(user)
+ context 'with skipping of subresources' do
+ before do
+ described_class.new(user).execute(@group_member, skip_subresources: true)
+ end
- group_member = create(:group_member, :developer, group: group, user: member_user)
+ it 'removes the group membership' do
+ expect(group.members.map(&:user)).not_to include(member_user)
+ end
- described_class.new(user).execute(group_member)
- end
+ it 'does not remove the project membership' do
+ expect(group_project.members.map(&:user)).to include(member_user)
+ end
- it 'removes the project membership' do
- expect(group_project.members.map(&:user)).not_to include(member_user)
- end
+ it 'does not remove the subgroup membership' do
+ expect(subgroup.members.map(&:user)).to include(member_user)
+ end
- it 'removes the group membership' do
- expect(group.members.map(&:user)).not_to include(member_user)
- end
+ it 'does not remove the subsubgroup membership' do
+ expect(subsubgroup.members.map(&:user)).to include(member_user)
+ end
- it 'removes the subgroup membership' do
- expect(subgroup.members.map(&:user)).not_to include(member_user)
- end
+ it 'does not remove the subsubproject membership' do
+ expect(subsubproject.members.map(&:user)).to include(member_user)
+ end
- it 'removes the subsubgroup membership' do
- expect(subsubgroup.members.map(&:user)).not_to include(member_user)
- end
+ it 'does not remove the user from the control project' do
+ expect(control_project.members.map(&:user)).to include(user)
+ end
+ end
- it 'removes the subsubproject membership' do
- expect(subsubproject.members.map(&:user)).not_to include(member_user)
- end
+ context 'without skipping of subresources' do
+ before do
+ described_class.new(user).execute(@group_member, skip_subresources: false)
+ end
- it 'does not remove the user from the control project' do
- expect(control_project.members.map(&:user)).to include(user)
- end
+ it 'removes the project membership' do
+ expect(group_project.members.map(&:user)).not_to include(member_user)
+ end
- it 'removes group members invited by deleted user' do
- expect(group.members.not_accepted_invitations_by_user(member_user)).to be_empty
- end
+ it 'removes the group membership' do
+ expect(group.members.map(&:user)).not_to include(member_user)
+ end
- it 'removes project members invited by deleted user' do
- expect(group_project.members.not_accepted_invitations_by_user(member_user)).to be_empty
- end
+ it 'removes the subgroup membership' do
+ expect(subgroup.members.map(&:user)).not_to include(member_user)
+ end
+
+ it 'removes the subsubgroup membership' do
+ expect(subsubgroup.members.map(&:user)).not_to include(member_user)
+ end
+
+ it 'removes the subsubproject membership' do
+ expect(subsubproject.members.map(&:user)).not_to include(member_user)
+ end
- it 'removes subgroup members invited by deleted user' do
- expect(subgroup.members.not_accepted_invitations_by_user(member_user)).to be_empty
+ it 'does not remove the user from the control project' do
+ expect(control_project.members.map(&:user)).to include(user)
+ end
+ end
end
- it 'removes subproject members invited by deleted user' do
- expect(subsubproject.members.not_accepted_invitations_by_user(member_user)).to be_empty
+ context 'with invites' do
+ before do
+ create(:group_member, :developer, group: subsubgroup, user: member_user)
+ create(:project_member, :invited, project: group_project, created_by: member_user)
+ create(:group_member, :invited, group: group, created_by: member_user)
+ create(:project_member, :invited, project: subsubproject, created_by: member_user)
+ create(:group_member, :invited, group: subgroup, created_by: member_user)
+
+ subsubproject.add_developer(member_user)
+ control_project.add_maintainer(user)
+ group.add_owner(user)
+
+ @group_member = create(:group_member, :developer, group: group, user: member_user)
+ end
+
+ context 'with skipping of subresources' do
+ before do
+ described_class.new(user).execute(@group_member, skip_subresources: true)
+ end
+
+ it 'does not remove group members invited by deleted user' do
+ expect(group.members.not_accepted_invitations_by_user(member_user)).not_to be_empty
+ end
+
+ it 'does not remove project members invited by deleted user' do
+ expect(group_project.members.not_accepted_invitations_by_user(member_user)).not_to be_empty
+ end
+
+ it 'does not remove subgroup members invited by deleted user' do
+ expect(subgroup.members.not_accepted_invitations_by_user(member_user)).not_to be_empty
+ end
+
+ it 'does not remove subproject members invited by deleted user' do
+ expect(subsubproject.members.not_accepted_invitations_by_user(member_user)).not_to be_empty
+ end
+ end
+
+ context 'without skipping of subresources' do
+ before do
+ described_class.new(user).execute(@group_member, skip_subresources: false)
+ end
+
+ it 'removes group members invited by deleted user' do
+ expect(group.members.not_accepted_invitations_by_user(member_user)).to be_empty
+ end
+
+ it 'removes project members invited by deleted user' do
+ expect(group_project.members.not_accepted_invitations_by_user(member_user)).to be_empty
+ end
+
+ it 'removes subgroup members invited by deleted user' do
+ expect(subgroup.members.not_accepted_invitations_by_user(member_user)).to be_empty
+ end
+
+ it 'removes subproject members invited by deleted user' do
+ expect(subsubproject.members.not_accepted_invitations_by_user(member_user)).to be_empty
+ end
+ end
end
end
diff --git a/spec/support/helpers/stub_env.rb b/spec/support/helpers/stub_env.rb
index 8107ffc939f..5f344f8fb52 100644
--- a/spec/support/helpers/stub_env.rb
+++ b/spec/support/helpers/stub_env.rb
@@ -14,7 +14,7 @@ module StubENV
private
- STUBBED_KEY = '__STUBBED__'.freeze
+ STUBBED_KEY = '__STUBBED__'
def add_stubbed_value(key, value)
allow(ENV).to receive(:[]).with(key).and_return(value)
diff --git a/spec/support/helpers/stub_requests.rb b/spec/support/helpers/stub_requests.rb
index 473f07dd413..a3810323fee 100644
--- a/spec/support/helpers/stub_requests.rb
+++ b/spec/support/helpers/stub_requests.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module StubRequests
- IP_ADDRESS_STUB = '8.8.8.9'.freeze
+ IP_ADDRESS_STUB = '8.8.8.9'
# Fully stubs a request using WebMock class. This class also
# stubs the IP address the URL is translated to (DNS lookup).
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 8a1007ff11f..9e283b2dd3e 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -92,7 +92,7 @@ module TestEnv
}.freeze
TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze
- REPOS_STORAGE = 'default'.freeze
+ REPOS_STORAGE = 'default'
SECOND_STORAGE_PATH = Rails.root.join('tmp', 'tests', 'second_storage')
# Test environment
diff --git a/spec/support/shared_examples/features/search_settings_shared_examples.rb b/spec/support/shared_examples/features/search_settings_shared_examples.rb
index e45dcd14670..dda780690b2 100644
--- a/spec/support/shared_examples/features/search_settings_shared_examples.rb
+++ b/spec/support/shared_examples/features/search_settings_shared_examples.rb
@@ -35,23 +35,3 @@ RSpec.shared_examples 'can highlight results' do |search_term|
end
end
end
-
-RSpec.shared_examples 'can search settings with feature flag check' do |search_term, non_match_section|
- let(:flag) { true }
-
- before do
- stub_feature_flags(search_settings_in_page: flag)
-
- visit(visit_path)
- end
-
- context 'with feature flag on' do
- it_behaves_like 'can search settings', search_term, non_match_section
- end
-
- context 'with feature flag off' do
- let(:flag) { false }
-
- it_behaves_like 'cannot search settings'
- end
-end
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 15976eed021..eb86b7c37d5 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -100,7 +100,7 @@ RSpec.shared_examples 'job token for package GET requests' do
end
end
-RSpec.shared_examples 'job token for package uploads' do
+RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: false|
context 'with job token headers' do
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_headers) }
@@ -111,6 +111,17 @@ RSpec.shared_examples 'job token for package uploads' do
context 'valid token' do
it_behaves_like 'returning response status', :success
+
+ unless authorize_endpoint
+ it 'creates a package with build info' do
+ expect { subject }.to change { Packages::Package.count }.by(1)
+
+ pkg = ::Packages::Package.order_created
+ .last
+
+ expect(pkg.build_infos).to be
+ end
+ end
end
context 'invalid token' do
diff --git a/spec/views/layouts/profile.html.haml_spec.rb b/spec/views/layouts/profile.html.haml_spec.rb
index 93f8a075209..77474555771 100644
--- a/spec/views/layouts/profile.html.haml_spec.rb
+++ b/spec/views/layouts/profile.html.haml_spec.rb
@@ -19,21 +19,8 @@ RSpec.describe 'layouts/profile' do
.with({ locals: { container_class: 'gl-my-5' } })
end
- context 'when search_settings_in_page feature flag is on' do
- it 'displays the search settings entry point' do
- render
- expect(rendered).to include('js-search-settings-app')
- end
- end
-
- context 'when search_settings_in_page feature flag is off' do
- before do
- stub_feature_flags(search_settings_in_page: false)
- end
-
- it 'does not display the search settings entry point' do
- render
- expect(rendered).not_to include('js-search-settings-app')
- end
+ it 'displays the search settings entry point' do
+ render
+ expect(rendered).to include('js-search-settings-app')
end
end
diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb
index dd32835aba1..9291b725af2 100644
--- a/tooling/danger/project_helper.rb
+++ b/tooling/danger/project_helper.rb
@@ -30,7 +30,7 @@ module Tooling
specs
].freeze
- MESSAGE_PREFIX = '==>'.freeze
+ MESSAGE_PREFIX = '==>'
# First-match win, so be sure to put more specific regex at the top...
CATEGORIES = {
diff --git a/tooling/lib/tooling/test_map_packer.rb b/tooling/lib/tooling/test_map_packer.rb
index 520d69610eb..d74edb9500f 100644
--- a/tooling/lib/tooling/test_map_packer.rb
+++ b/tooling/lib/tooling/test_map_packer.rb
@@ -2,7 +2,7 @@
module Tooling
class TestMapPacker
- SEPARATOR = '/'.freeze
+ SEPARATOR = '/'
MARKER = 1
def pack(map)
diff --git a/workhorse/internal/api/api.go b/workhorse/internal/api/api.go
index a420288a95a..e434a847bf2 100644
--- a/workhorse/internal/api/api.go
+++ b/workhorse/internal/api/api.go
@@ -151,7 +151,7 @@ type Response struct {
MaximumSize int64
}
-// singleJoiningSlash is taken from reverseproxy.go:NewSingleHostReverseProxy
+// singleJoiningSlash is taken from reverseproxy.go:singleJoiningSlash
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
@@ -164,14 +164,36 @@ func singleJoiningSlash(a, b string) string {
return a + b
}
+// joinURLPath is taken from reverseproxy.go:joinURLPath
+func joinURLPath(a *url.URL, b string) (path string, rawpath string) {
+ if a.RawPath == "" && b == "" {
+ return singleJoiningSlash(a.Path, b), ""
+ }
+
+ // Same as singleJoiningSlash, but uses EscapedPath to determine
+ // whether a slash should be added
+ apath := a.EscapedPath()
+ bpath := b
+
+ aslash := strings.HasSuffix(apath, "/")
+ bslash := strings.HasPrefix(bpath, "/")
+
+ switch {
+ case aslash && bslash:
+ return a.Path + bpath[1:], apath + bpath[1:]
+ case !aslash && !bslash:
+ return a.Path + "/" + bpath, apath + "/" + bpath
+ }
+ return a.Path + bpath, apath + bpath
+}
+
// rebaseUrl is taken from reverseproxy.go:NewSingleHostReverseProxy
func rebaseUrl(url *url.URL, onto *url.URL, suffix string) *url.URL {
newUrl := *url
newUrl.Scheme = onto.Scheme
newUrl.Host = onto.Host
- if suffix != "" {
- newUrl.Path = singleJoiningSlash(url.Path, suffix)
- }
+ newUrl.Path, newUrl.RawPath = joinURLPath(url, suffix)
+
if onto.RawQuery == "" || newUrl.RawQuery == "" {
newUrl.RawQuery = onto.RawQuery + newUrl.RawQuery
} else {
diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go
index fb8a07a8031..b77cb06d1d0 100644
--- a/workhorse/internal/upstream/routes.go
+++ b/workhorse/internal/upstream/routes.go
@@ -57,6 +57,7 @@ const (
ciAPIPattern = `^/ci/api/`
gitProjectPattern = `^/.+\.git/`
projectPattern = `^/([^/]+/){1,}[^/]+/`
+ apiProjectPattern = apiPattern + `v4/projects/[^/]+/` // API: Projects can be encoded via group%2Fsubgroup%2Fproject
snippetUploadPattern = `^/uploads/personal_snippet`
userUploadPattern = `^/uploads/user`
importPattern = `^/import/`
@@ -253,32 +254,39 @@ func configureRoutes(u *upstream) {
u.route("", apiPattern+`v4/jobs/request\z`, ciAPILongPolling),
u.route("", ciAPIPattern+`v1/builds/register.json\z`, ciAPILongPolling),
+ // Not all API endpoints support encoded project IDs
+ // (e.g. `group%2Fproject`), but for the sake of consistency we
+ // use the apiProjectPattern regex throughout. API endpoints
+ // that do not support this will return 400 regardless of
+ // whether they are accelerated by Workhorse or not. See
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56731.
+
// Maven Artifact Repository
- u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/maven/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
+ u.route("PUT", apiProjectPattern+`packages/maven/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
// Conan Artifact Repository
u.route("PUT", apiPattern+`v4/packages/conan/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
- u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/conan/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
+ u.route("PUT", apiProjectPattern+`packages/conan/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
// Generic Packages Repository
- u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/generic/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
+ u.route("PUT", apiProjectPattern+`packages/generic/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
// NuGet Artifact Repository
- u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/nuget/`, upload.Accelerate(api, signingProxy, preparers.packages)),
+ u.route("PUT", apiProjectPattern+`packages/nuget/`, upload.Accelerate(api, signingProxy, preparers.packages)),
// PyPI Artifact Repository
- u.route("POST", apiPattern+`v4/projects/[0-9]+/packages/pypi`, upload.Accelerate(api, signingProxy, preparers.packages)),
+ u.route("POST", apiProjectPattern+`packages/pypi`, upload.Accelerate(api, signingProxy, preparers.packages)),
// Debian Artifact Repository
- u.route("PUT", apiPattern+`v4/projects/[0-9]+/packages/debian/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
+ u.route("PUT", apiProjectPattern+`packages/debian/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
// Gem Artifact Repository
- u.route("POST", apiPattern+`v4/projects/[0-9]+/packages/rubygems/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
+ u.route("POST", apiProjectPattern+`packages/rubygems/`, upload.BodyUploader(api, signingProxy, preparers.packages)),
// We are porting API to disk acceleration
// we need to declare each routes until we have fixed all the routes on the rails codebase.
// Overall status can be seen at https://gitlab.com/groups/gitlab-org/-/epics/1802#current-status
- u.route("POST", apiPattern+`v4/projects/[0-9]+/wikis/attachments\z`, uploadAccelerateProxy),
+ u.route("POST", apiProjectPattern+`wikis/attachments\z`, uploadAccelerateProxy),
u.route("POST", apiPattern+`graphql\z`, uploadAccelerateProxy),
u.route("POST", apiPattern+`v4/groups/import`, upload.Accelerate(api, signingProxy, preparers.uploads)),
u.route("POST", apiPattern+`v4/projects/import`, upload.Accelerate(api, signingProxy, preparers.uploads)),
@@ -289,7 +297,7 @@ func configureRoutes(u *upstream) {
u.route("POST", importPattern+`gitlab_group`, upload.Accelerate(api, signingProxy, preparers.uploads)),
// Metric image upload
- u.route("POST", apiPattern+`v4/projects/[0-9]+/issues/[0-9]+/metric_images\z`, upload.Accelerate(api, signingProxy, preparers.uploads)),
+ u.route("POST", apiProjectPattern+`issues/[0-9]+/metric_images\z`, upload.Accelerate(api, signingProxy, preparers.uploads)),
// Requirements Import via UI upload acceleration
u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, upload.Accelerate(api, signingProxy, preparers.uploads)),
diff --git a/workhorse/upload_test.go b/workhorse/upload_test.go
index 6d118119dff..fdd4605dbc4 100644
--- a/workhorse/upload_test.go
+++ b/workhorse/upload_test.go
@@ -41,7 +41,7 @@ func testArtifactsUpload(t *testing.T, uploadArtifacts uploadArtifactsFunction)
reqBody, contentType, err := multipartBodyWithFile()
require.NoError(t, err)
- ts := signedUploadTestServer(t, nil)
+ ts := signedUploadTestServer(t, nil, nil)
defer ts.Close()
ws := startWorkhorseServer(ts.URL)
@@ -66,7 +66,7 @@ func expectSignedRequest(t *testing.T, r *http.Request) {
require.NoError(t, err)
}
-func uploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest.Server {
+func uploadTestServer(t *testing.T, authorizeTests func(r *http.Request), extraTests func(r *http.Request)) *httptest.Server {
return testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/authorize") {
expectSignedRequest(t, r)
@@ -74,6 +74,10 @@ func uploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest.
w.Header().Set("Content-Type", api.ResponseContentType)
_, err := fmt.Fprintf(w, `{"TempPath":"%s"}`, scratchDir)
require.NoError(t, err)
+
+ if authorizeTests != nil {
+ authorizeTests(r)
+ }
return
}
@@ -91,10 +95,10 @@ func uploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest.
})
}
-func signedUploadTestServer(t *testing.T, extraTests func(r *http.Request)) *httptest.Server {
+func signedUploadTestServer(t *testing.T, authorizeTests func(r *http.Request), extraTests func(r *http.Request)) *httptest.Server {
t.Helper()
- return uploadTestServer(t, func(r *http.Request) {
+ return uploadTestServer(t, authorizeTests, func(r *http.Request) {
expectSignedRequest(t, r)
if extraTests != nil {
@@ -113,21 +117,39 @@ func TestAcceleratedUpload(t *testing.T) {
{"POST", `/uploads/personal_snippet`, true},
{"POST", `/uploads/user`, true},
{"POST", `/api/v4/projects/1/wikis/attachments`, false},
+ {"POST", `/api/v4/projects/group%2Fproject/wikis/attachments`, false},
+ {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/wikis/attachments`, false},
{"POST", `/api/graphql`, false},
{"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true},
+ {"PUT", "/api/v4/projects/group%2Fproject/packages/nuget/v1/files", true},
+ {"PUT", "/api/v4/projects/group%2Fsubgroup%2Fproject/packages/nuget/v1/files", true},
{"POST", `/api/v4/groups/import`, true},
+ {"POST", `/api/v4/groups/import/`, true},
{"POST", `/api/v4/projects/import`, true},
+ {"POST", `/api/v4/projects/import/`, true},
{"POST", `/import/gitlab_project`, true},
+ {"POST", `/import/gitlab_project/`, true},
{"POST", `/import/gitlab_group`, true},
+ {"POST", `/import/gitlab_group/`, true},
{"POST", `/api/v4/projects/9001/packages/pypi`, true},
+ {"POST", `/api/v4/projects/group%2Fproject/packages/pypi`, true},
+ {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/packages/pypi`, true},
{"POST", `/api/v4/projects/9001/issues/30/metric_images`, true},
+ {"POST", `/api/v4/projects/group%2Fproject/issues/30/metric_images`, true},
+ {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/issues/30/metric_images`, true},
{"POST", `/my/project/-/requirements_management/requirements/import_csv`, true},
+ {"POST", `/my/project/-/requirements_management/requirements/import_csv/`, true},
}
for _, tt := range tests {
t.Run(tt.resource, func(t *testing.T) {
ts := uploadTestServer(t,
func(r *http.Request) {
+ resource := strings.TrimRight(tt.resource, "/")
+ // Validate %2F characters haven't been unescaped
+ require.Equal(t, resource+"/authorize", r.URL.String())
+ },
+ func(r *http.Request) {
if tt.signedFinalization {
expectSignedRequest(t, r)
}
@@ -186,6 +208,55 @@ func multipartBodyWithFile() (io.Reader, string, error) {
return result, writer.FormDataContentType(), writer.Close()
}
+func unacceleratedUploadTestServer(t *testing.T) *httptest.Server {
+ return testhelper.TestServerWithHandler(regexp.MustCompile(`.`), func(w http.ResponseWriter, r *http.Request) {
+ require.False(t, strings.HasSuffix(r.URL.Path, "/authorize"))
+ require.Empty(t, r.Header.Get(upload.RewrittenFieldsHeader))
+
+ w.WriteHeader(200)
+ })
+}
+
+func TestUnacceleratedUploads(t *testing.T) {
+ tests := []struct {
+ method string
+ resource string
+ }{
+ {"POST", `/api/v4/projects/group/subgroup/project/wikis/attachments`},
+ {"POST", `/api/v4/projects/group/project/wikis/attachments`},
+ {"PUT", "/api/v4/projects/group/subgroup/project/packages/nuget/v1/files"},
+ {"PUT", "/api/v4/projects/group/project/packages/nuget/v1/files"},
+ {"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
+ {"POST", `/api/v4/projects/group/project/packages/pypi`},
+ {"POST", `/api/v4/projects/group/subgroup/project/packages/pypi`},
+ {"POST", `/api/v4/projects/group/project/issues/30/metric_images`},
+ {"POST", `/api/v4/projects/group/subgroup/project/issues/30/metric_images`},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.resource, func(t *testing.T) {
+ ts := unacceleratedUploadTestServer(t)
+
+ defer ts.Close()
+ ws := startWorkhorseServer(ts.URL)
+ defer ws.Close()
+
+ reqBody, contentType, err := multipartBodyWithFile()
+ require.NoError(t, err)
+
+ req, err := http.NewRequest(tt.method, ws.URL+tt.resource, reqBody)
+ require.NoError(t, err)
+
+ req.Header.Set("Content-Type", contentType)
+ resp, err := http.DefaultClient.Do(req)
+ require.NoError(t, err)
+ require.Equal(t, 200, resp.StatusCode)
+
+ resp.Body.Close()
+ })
+ }
+}
+
func TestBlockingRewrittenFieldsHeader(t *testing.T) {
canary := "untrusted header passed by user"
testCases := []struct {
@@ -433,6 +504,11 @@ func TestPackageFilesUpload(t *testing.T) {
{"PUT", "/api/v4/projects/2412/packages/generic/mypackage/0.0.1/myfile.tar.gz"},
{"PUT", "/api/v4/projects/2412/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb"},
{"POST", "/api/v4/projects/2412/packages/rubygems/api/v1/gems/sample.gem"},
+ {"PUT", "/api/v4/projects/group%2Fproject/packages/conan/v1/files"},
+ {"PUT", "/api/v4/projects/group%2Fproject/packages/maven/v1/files"},
+ {"PUT", "/api/v4/projects/group%2Fproject/packages/generic/mypackage/0.0.1/myfile.tar.gz"},
+ {"PUT", "/api/v4/projects/group%2Fproject/packages/debian/libsample0_1.2.3~alpha2-1_amd64.deb"},
+ {"POST", "/api/v4/projects/group%2Fproject/packages/rubygems/api/v1/gems/sample.gem"},
}
for _, r := range routes {