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-07-28 15:10:41 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-28 15:10:41 +0300
commitb1e352740bd52771b419829abef0a0ad73141ac1 (patch)
treee86202376eb85b6314ab90fe028c0889098b05ef
parentaeee5b6a212eafefe3c994fb3731ccfca590a6ba (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml2
-rw-r--r--CHANGELOG.md16
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js1
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue15
-rw-r--r--app/assets/javascripts/integrations/overrides/index.js23
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue64
-rw-r--r--app/assets/javascripts/pages/admin/integrations/overrides/index.js3
-rw-r--r--app/assets/javascripts/runner/components/runner_registration_token_reset.vue41
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue16
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js25
-rw-r--r--app/controllers/admin/integrations_controller.rb19
-rw-r--r--app/controllers/concerns/integrations_actions.rb3
-rw-r--r--app/controllers/groups/settings/integrations_controller.rb4
-rw-r--r--app/helpers/groups_helper.rb28
-rw-r--r--app/helpers/integrations_helper.rb10
-rw-r--r--app/models/concerns/has_integrations.rb12
-rw-r--r--app/models/concerns/time_trackable.rb2
-rw-r--r--app/models/integration.rb1
-rw-r--r--app/models/project.rb5
-rw-r--r--app/serializers/integrations/project_entity.rb15
-rw-r--r--app/serializers/integrations/project_serializer.rb9
-rw-r--r--app/services/packages/debian/generate_distribution_service.rb39
-rw-r--r--app/services/packages/debian/sign_distribution_service.rb6
-rw-r--r--app/views/groups/runners/index.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group_menus.html.haml17
-rw-r--r--app/views/shared/integrations/_tabs.html.haml14
-rw-r--r--app/views/shared/integrations/overrides.html.haml10
-rw-r--r--app/workers/clusters/applications/deactivate_service_worker.rb8
-rw-r--r--config/feature_flags/development/instance_level_integration_overrides.yml8
-rw-r--r--config/feature_flags/development/vuln_report_new_project_filter.yml8
-rw-r--r--config/routes/admin.rb1
-rw-r--r--doc/administration/geo/replication/object_storage.md5
-rw-r--r--doc/api/graphql/reference/index.md22
-rw-r--r--doc/architecture/blueprints/database_scaling/size-limits.md176
-rw-r--r--doc/user/packages/pypi_repository/index.md11
-rw-r--r--doc/user/project/clusters/add_remove_clusters.md2
-rw-r--r--lib/api/pypi_packages.rb4
-rw-r--r--lib/gitlab/reactive_cache_set_cache.rb5
-rw-r--r--lib/gitlab/repository_set_cache.rb5
-rw-r--r--lib/gitlab/set_cache.rb6
-rw-r--r--lib/gitlab/usage_data.rb6
-rw-r--r--lib/sidebars/groups/menus/merge_requests_menu.rb58
-rw-r--r--lib/sidebars/groups/panel.rb1
-rw-r--r--locale/gitlab.pot22
-rw-r--r--qa/qa/page/merge_request/show.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb4
-rw-r--r--spec/controllers/admin/integrations_controller_spec.rb48
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js40
-rw-r--r--spec/frontend/admin/signup_restrictions/mock_data.js2
-rw-r--r--spec/frontend/runner/components/runner_registration_token_reset_spec.js73
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js15
-rw-r--r--spec/helpers/groups_helper_spec.rb19
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb6
-rw-r--r--spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb36
-rw-r--r--spec/models/concerns/has_integrations_spec.rb8
-rw-r--r--spec/models/integration_spec.rb18
-rw-r--r--spec/models/issue_spec.rb1
-rw-r--r--spec/models/project_spec.rb56
-rw-r--r--spec/requests/api/pypi_packages_spec.rb12
-rw-r--r--spec/serializers/integrations/project_entity_spec.rb26
-rw-r--r--spec/serializers/integrations/project_serializer_spec.rb9
-rw-r--r--spec/services/packages/debian/generate_distribution_service_spec.rb8
-rw-r--r--spec/services/packages/debian/sign_distribution_service_spec.rb5
-rw-r--r--spec/support/shared_examples/helpers/groups_shared_examples.rb53
-rw-r--r--spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb322
-rw-r--r--spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb14
67 files changed, 1058 insertions, 478 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 318ac20435e..de484f2ce84 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -242,7 +242,7 @@ jest:
- tmp/tests/frontend/
reports:
junit: junit_jest.xml
- parallel: 4
+ parallel: 5
jest-integration:
extends:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b3261e99ff..84c09f76639 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 14.1.1 (2021-07-28)
+
+### Added (1 change)
+
+- [RackAttack: extend basic authentication detection for rate limiting](gitlab-org/gitlab@ad521c88bfa8da185380397aa2e6e8972a28b04e) ([merge request](gitlab-org/gitlab!66726))
+
+### Fixed (3 changes)
+
+- [Prevent terms from being created if blank](gitlab-org/gitlab@29e5ebe23869cfe1325d8f7ab2ec17a3a8670f61) ([merge request](gitlab-org/gitlab!66726))
+- [Fix: Sidekiq workers delete each other's metrics](gitlab-org/gitlab@d6d8ed55392a90cc55aa6213ebae80008d0df3e0) ([merge request](gitlab-org/gitlab!66726))
+- [Resolve "Bulk dismissal checkboxes don't appear on group vulnerability report"](gitlab-org/gitlab@77b2cf8b935aba08f23c00cf5fdc746849a65e74) ([merge request](gitlab-org/gitlab!66726)) **GitLab Enterprise Edition**
+
+### Other (1 change)
+
+- [Revert backfill on ci_build_trace_sections](gitlab-org/gitlab@a67a8d734440d50c5fdbb0c559b5d2a2f6e48fae) ([merge request](gitlab-org/gitlab!66726))
+
## 14.1.0 (2021-07-21)
### Added (123 changes)
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index aad7712a9f0..95ddf12afae 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -5,6 +5,7 @@ export const TYPE_ITERATION = 'Iteration';
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
export const TYPE_MERGE_REQUEST = 'MergeRequest';
export const TYPE_MILESTONE = 'Milestone';
+export const TYPE_PROJECT = 'Project';
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
new file mode 100644
index 00000000000..bfb16779854
--- /dev/null
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -0,0 +1,15 @@
+<script>
+export default {
+ name: 'IntegrationOverrides',
+ props: {
+ overridesPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div></div>
+</template>
diff --git a/app/assets/javascripts/integrations/overrides/index.js b/app/assets/javascripts/integrations/overrides/index.js
new file mode 100644
index 00000000000..0f03b23ba21
--- /dev/null
+++ b/app/assets/javascripts/integrations/overrides/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import IntegrationOverrides from './components/integration_overrides.vue';
+
+export default () => {
+ const el = document.querySelector('.js-vue-integration-overrides');
+
+ if (!el) {
+ return null;
+ }
+
+ const { overridesPath } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(IntegrationOverrides, {
+ props: {
+ overridesPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index 9850113d4be..c2510a16d2f 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -11,7 +11,7 @@ import {
} from '@gitlab/ui';
import { toSafeInteger } from 'lodash';
import csrf from '~/lib/utils/csrf';
-import { __, s__, sprintf } from '~/locale';
+import { __, n__, s__, sprintf } from '~/locale';
import SignupCheckbox from './signup_checkbox.vue';
const DENYLIST_TYPE_RAW = 'raw';
@@ -51,6 +51,7 @@ export default {
'supportedSyntaxLinkUrl',
'emailRestrictions',
'afterSignUpText',
+ 'pendingUserCount',
],
data() {
return {
@@ -105,8 +106,9 @@ export default {
canUsersBeAccidentallyApproved() {
const hasUserCapBeenToggledOff =
this.requireAdminApprovalAfterUserSignup && !this.form.requireAdminApproval;
+ const currentlyPendingUsers = this.pendingUserCount > 0;
- return this.hasUserCapBeenIncreased || hasUserCapBeenToggledOff;
+ return (this.hasUserCapBeenIncreased || hasUserCapBeenToggledOff) && currentlyPendingUsers;
},
signupEnabledHelpText() {
const text = sprintf(
@@ -132,13 +134,39 @@ export default {
return text;
},
+ approveUsersModal() {
+ const { pendingUserCount } = this;
+
+ return {
+ id: 'signup-settings-modal',
+ text: n__(
+ 'ApplicationSettings|By making this change, you will automatically approve %d user with the pending approval status.',
+ 'ApplicationSettings|By making this change, you will automatically approve %d users with the pending approval status.',
+ pendingUserCount,
+ ),
+ actionPrimary: {
+ text: n__(
+ 'ApplicationSettings|Approve %d user',
+ 'ApplicationSettings|Approve %d users',
+ pendingUserCount,
+ ),
+ attributes: {
+ variant: 'confirm',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ title: s__('ApplicationSettings|Approve users in the pending approval status?'),
+ };
+ },
},
watch: {
showModal(value) {
if (value === true) {
- this.$refs[this.$options.modal.id].show();
+ this.$refs[this.approveUsersModal.id].show();
} else {
- this.$refs[this.$options.modal.id].hide();
+ this.$refs[this.approveUsersModal.id].hide();
}
},
},
@@ -196,22 +224,6 @@ export default {
afterSignUpTextGroupLabel: s__('ApplicationSettings|After sign up text'),
afterSignUpTextGroupDescription: s__('ApplicationSettings|Markdown enabled'),
},
- modal: {
- id: 'signup-settings-modal',
- actionPrimary: {
- text: s__('ApplicationSettings|Approve users'),
- attributes: {
- variant: 'confirm',
- },
- },
- actionCancel: {
- text: __('Cancel'),
- },
- title: s__('ApplicationSettings|Approve all users in the pending approval status?'),
- text: s__(
- 'ApplicationSettings|By making this change, you will automatically approve all users in pending approval status.',
- ),
- },
};
</script>
@@ -403,15 +415,15 @@ export default {
</gl-button>
<gl-modal
- :ref="$options.modal.id"
- :modal-id="$options.modal.id"
- :action-cancel="$options.modal.actionCancel"
- :action-primary="$options.modal.actionPrimary"
- :title="$options.modal.title"
+ :ref="approveUsersModal.id"
+ :modal-id="approveUsersModal.id"
+ :action-cancel="approveUsersModal.actionCancel"
+ :action-primary="approveUsersModal.actionPrimary"
+ :title="approveUsersModal.title"
@primary="submitForm"
@hide="modalHideHandler"
>
- {{ $options.modal.text }}
+ {{ approveUsersModal.text }}
</gl-modal>
</form>
</template>
diff --git a/app/assets/javascripts/pages/admin/integrations/overrides/index.js b/app/assets/javascripts/pages/admin/integrations/overrides/index.js
new file mode 100644
index 00000000000..b1504709144
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/integrations/overrides/index.js
@@ -0,0 +1,3 @@
+import initIntegrationOverrides from '~/integrations/overrides';
+
+initIntegrationOverrides();
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
index 2335faa4f85..cdf14abd4f9 100644
--- a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
+++ b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
@@ -1,6 +1,8 @@
<script>
import { GlButton } from '@gitlab/ui';
import createFlash, { FLASH_TYPES } from '~/flash';
+import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
@@ -11,6 +13,14 @@ export default {
components: {
GlButton,
},
+ inject: {
+ groupId: {
+ default: null,
+ },
+ projectId: {
+ default: null,
+ },
+ },
props: {
type: {
type: String,
@@ -25,7 +35,28 @@ export default {
loading: false,
};
},
- computed: {},
+ computed: {
+ resetTokenInput() {
+ switch (this.type) {
+ case INSTANCE_TYPE:
+ return {
+ type: this.type,
+ };
+ case GROUP_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_GROUP, this.groupId),
+ type: this.type,
+ };
+ case PROJECT_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_PROJECT, this.projectId),
+ type: this.type,
+ };
+ default:
+ return null;
+ }
+ },
+ },
methods: {
async resetToken() {
// TODO Replace confirmation with gl-modal
@@ -44,13 +75,7 @@ export default {
} = await this.$apollo.mutate({
mutation: runnersRegistrationTokenResetMutation,
variables: {
- // TODO Currently INTANCE_TYPE only is supported
- // In future iterations this component will support
- // other registration token types.
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/19819
- input: {
- type: this.type,
- },
+ input: this.resetTokenInput,
},
});
if (errors && errors.length) {
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 92d881c43ea..07bbf60c453 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -1,10 +1,20 @@
<script>
+import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
+import { GROUP_TYPE } from '../constants';
export default {
components: {
+ RunnerManualSetupHelp,
RunnerTypeHelp,
},
+ props: {
+ registrationToken: {
+ type: String,
+ required: true,
+ },
+ },
+ GROUP_TYPE,
};
</script>
@@ -14,6 +24,12 @@ export default {
<div class="col-sm-6">
<runner-type-help />
</div>
+ <div class="col-sm-6">
+ <runner-manual-setup-help
+ :registration-token="registrationToken"
+ :type="$options.GROUP_TYPE"
+ />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index 5a72b09de3a..e14c583d73e 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -1,6 +1,10 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import GroupRunnersApp from './group_runners_app.vue';
+Vue.use(VueApollo);
+
export const initGroupRunners = (selector = '#js-group-runners') => {
const el = document.querySelector(selector);
@@ -8,10 +12,29 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
return null;
}
+ const { registrationToken, groupId } = el.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
+ });
+
return new Vue({
el,
+ apolloProvider,
+ provide: {
+ groupId,
+ },
render(h) {
- return h(GroupRunnersApp);
+ return h(GroupRunnersApp, {
+ props: {
+ registrationToken,
+ },
+ });
},
});
};
diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb
index 76c1c46e0e8..e273c026993 100644
--- a/app/controllers/admin/integrations_controller.rb
+++ b/app/controllers/admin/integrations_controller.rb
@@ -2,19 +2,32 @@
class Admin::IntegrationsController < Admin::ApplicationController
include IntegrationsActions
- include IntegrationsHelper
before_action :not_found, unless: -> { instance_level_integrations? }
feature_category :integrations
+ def overrides
+ return render_404 unless instance_level_integration_overrides?
+
+ respond_to do |format|
+ format.json do
+ projects = Project.with_active_integration(integration.class).merge(::Integration.not_inherited)
+ serializer = ::Integrations::ProjectSerializer.new.with_pagination(request, response)
+
+ render json: serializer.represent(projects)
+ end
+ format.html { render 'shared/integrations/overrides' }
+ end
+ end
+
private
def find_or_initialize_non_project_specific_integration(name)
Integration.find_or_initialize_non_project_specific_integration(name, instance: true)
end
- def scoped_edit_integration_path(integration)
- edit_admin_application_settings_integration_path(integration)
+ def instance_level_integration_overrides?
+ Feature.enabled?(:instance_level_integration_overrides, default_enabled: :yaml)
end
end
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
index f1fa5c845e2..dd066cc1b02 100644
--- a/app/controllers/concerns/integrations_actions.rb
+++ b/app/controllers/concerns/integrations_actions.rb
@@ -5,8 +5,9 @@ module IntegrationsActions
included do
include Integrations::Params
+ include IntegrationsHelper
- before_action :integration, only: [:edit, :update, :test]
+ before_action :integration, only: [:edit, :update, :overrides, :test]
end
def edit
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index 8e3b2cb5d1b..a7a1de03224 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -26,10 +26,6 @@ module Groups
def find_or_initialize_non_project_specific_integration(name)
Integration.find_or_initialize_non_project_specific_integration(name, group_id: group.id)
end
-
- def scoped_edit_integration_path(integration)
- edit_group_settings_integration_path(group, integration)
- end
end
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 34d1e951980..18f3f153aee 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -76,14 +76,6 @@ module GroupsHelper
.count
end
- def cached_issuables_count(group, type: nil)
- count_service = issuables_count_service_class(type)
- return unless count_service.present?
-
- issuables_count = count_service.new(group, current_user).count
- format_issuables_count(count_service, issuables_count)
- end
-
def group_dependency_proxy_url(group)
# The namespace path can include uppercase letters, which
# Docker doesn't allow. The proxy expects it to be downcased.
@@ -318,26 +310,6 @@ module GroupsHelper
def group_url_error_message
s_('GroupSettings|Please choose a group URL with no special characters or spaces.')
end
-
- def issuables_count_service_class(type)
- if type == :issues
- Groups::OpenIssuesCountService
- elsif type == :merge_requests
- Groups::MergeRequestsCountService
- end
- end
-
- def format_issuables_count(count_service, count)
- if count > count_service::CACHED_COUNT_THRESHOLD
- ActiveSupport::NumberHelper
- .number_to_human(
- count,
- units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u'
- )
- else
- number_with_delimiter(count)
- end
- end
end
GroupsHelper.prepend_mod_with('GroupsHelper')
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index ab305d822e8..734820f0e74 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -47,6 +47,10 @@ module IntegrationsHelper
end
end
+ def scoped_overrides_integration_path(integration, options = {})
+ overrides_admin_application_settings_integration_path(integration, options)
+ end
+
def scoped_test_integration_path(integration)
if @project.present?
test_project_service_path(@project, integration)
@@ -97,6 +101,12 @@ module IntegrationsHelper
form_data
end
+ def integration_overrides_data(integration)
+ {
+ overrides_path: scoped_overrides_integration_path(integration, format: :json)
+ }
+ end
+
def integration_list_data(integrations)
{
integrations: integrations.map { |i| serialize_integration(i) }.to_json
diff --git a/app/models/concerns/has_integrations.rb b/app/models/concerns/has_integrations.rb
index 25650ae56ad..76e03d68600 100644
--- a/app/models/concerns/has_integrations.rb
+++ b/app/models/concerns/has_integrations.rb
@@ -4,18 +4,6 @@ module HasIntegrations
extend ActiveSupport::Concern
class_methods do
- def with_custom_integration_for(integration, page = nil, per = nil)
- custom_integration_project_ids = Integration
- .select(:project_id)
- .where(type: integration.type)
- .where(inherit_from_id: nil)
- .where.not(project_id: nil)
- .page(page)
- .per(per)
-
- Project.where(id: custom_integration_project_ids)
- end
-
def without_integration(integration)
integrations = Integration
.select('1')
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index a638d62d79c..54fe9eac2bc 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -20,7 +20,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
- has_many :timelogs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
diff --git a/app/models/integration.rb b/app/models/integration.rb
index ff316146fa1..118b385cf30 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -78,6 +78,7 @@ class Integration < ApplicationRecord
scope :by_active_flag, -> (flag) { where(active: flag) }
scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
scope :inherit, -> { where.not(inherit_from_id: nil) }
+ scope :not_inherited, -> { where(inherit_from_id: nil) }
scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) }
scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 7032cd3a32c..9340e356399 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -540,7 +540,6 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
- scope :with_active_jira_integrations, -> { joins(:integrations).merge(::Integrations::Jira.active) }
scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :inc_routes, -> { includes(:route, namespace: :route) }
@@ -548,7 +547,9 @@ class Project < ApplicationRecord
scope :with_namespace, -> { includes(:namespace) }
scope :with_import_state, -> { includes(:import_state) }
scope :include_project_feature, -> { includes(:project_feature) }
- scope :with_integration, ->(integration) { joins(integration).eager_load(integration) }
+ scope :include_integration, -> (integration_association_name) { includes(integration_association_name) }
+ scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) }
+ scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
diff --git a/app/serializers/integrations/project_entity.rb b/app/serializers/integrations/project_entity.rb
new file mode 100644
index 00000000000..ee28c7c19c1
--- /dev/null
+++ b/app/serializers/integrations/project_entity.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Integrations
+ class ProjectEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :avatar_url
+ expose :full_name
+ expose :name
+
+ expose :full_path do |project|
+ project_path(project)
+ end
+ end
+end
diff --git a/app/serializers/integrations/project_serializer.rb b/app/serializers/integrations/project_serializer.rb
new file mode 100644
index 00000000000..b7cd266fcbf
--- /dev/null
+++ b/app/serializers/integrations/project_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Integrations
+ class ProjectSerializer < BaseSerializer
+ include WithPagination
+
+ entity Integrations::ProjectEntity
+ end
+end
diff --git a/app/services/packages/debian/generate_distribution_service.rb b/app/services/packages/debian/generate_distribution_service.rb
index caf1673e0a0..d3ffe75f6ee 100644
--- a/app/services/packages/debian/generate_distribution_service.rb
+++ b/app/services/packages/debian/generate_distribution_service.rb
@@ -161,28 +161,37 @@ module Packages
end
def generate_release
- @distribution.file = CarrierWaveStringFile.new(release_header + release_sums)
+ @distribution.key || @distribution.create_key(GenerateDistributionKeyService.new.execute)
+ @distribution.file = CarrierWaveStringFile.new(release_content)
+ @distribution.file_signature = SignDistributionService.new(@distribution, release_content, detach: true).execute
+ @distribution.signed_file = CarrierWaveStringFile.new(
+ SignDistributionService.new(@distribution, release_content).execute
+ )
@distribution.updated_at = release_date
@distribution.save!
end
- def release_header
- strong_memoize(:release_header) do
- [
- %w[origin label suite version codename].map do |attribute|
- rfc822_field(attribute.capitalize, @distribution.attributes[attribute])
- end,
- rfc822_field('Date', release_date.to_formatted_s(:rfc822)),
- valid_until_field,
- rfc822_field('NotAutomatic', !@distribution.automatic, !@distribution.automatic),
- rfc822_field('ButAutomaticUpgrades', @distribution.automatic_upgrades, !@distribution.automatic && @distribution.automatic_upgrades),
- rfc822_field('Architectures', @distribution.architectures.map { |architecture| architecture.name }.sort.join(' ')),
- rfc822_field('Components', @distribution.components.map { |component| component.name }.sort.join(' ')),
- rfc822_field('Description', @distribution.description)
- ].flatten.compact.join('')
+ def release_content
+ strong_memoize(:release_content) do
+ release_header + release_sums
end
end
+ def release_header
+ [
+ %w[origin label suite version codename].map do |attribute|
+ rfc822_field(attribute.capitalize, @distribution.attributes[attribute])
+ end,
+ rfc822_field('Date', release_date.to_formatted_s(:rfc822)),
+ valid_until_field,
+ rfc822_field('NotAutomatic', !@distribution.automatic, !@distribution.automatic),
+ rfc822_field('ButAutomaticUpgrades', @distribution.automatic_upgrades, !@distribution.automatic && @distribution.automatic_upgrades),
+ rfc822_field('Architectures', @distribution.architectures.map { |architecture| architecture.name }.sort.join(' ')),
+ rfc822_field('Components', @distribution.components.map { |component| component.name }.sort.join(' ')),
+ rfc822_field('Description', @distribution.description)
+ ].flatten.compact.join('')
+ end
+
def release_date
strong_memoize(:release_date) do
Time.now.utc
diff --git a/app/services/packages/debian/sign_distribution_service.rb b/app/services/packages/debian/sign_distribution_service.rb
index 643995dab67..7797f7e9c0a 100644
--- a/app/services/packages/debian/sign_distribution_service.rb
+++ b/app/services/packages/debian/sign_distribution_service.rb
@@ -5,10 +5,10 @@ module Packages
class SignDistributionService
include Gitlab::Utils::StrongMemoize
- def initialize(distribution, content, params: {})
+ def initialize(distribution, content, detach: false)
@distribution = distribution
@content = content
- @params = params
+ @detach = detach
end
def execute
@@ -16,7 +16,7 @@ module Packages
sig_mode = GPGME::GPGME_SIG_MODE_CLEAR
- sig_mode = GPGME::GPGME_SIG_MODE_DETACH if @params[:detach]
+ sig_mode = GPGME::GPGME_SIG_MODE_DETACH if @detach
Gitlab::Gpg.using_tmp_keychain do
GPGME::Ctx.new(
diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml
index 08be0f93d82..4e7bc99b1f0 100644
--- a/app/views/groups/runners/index.html.haml
+++ b/app/views/groups/runners/index.html.haml
@@ -3,4 +3,4 @@
%h2.page-title
= s_('Runners|Group Runners')
-#js-group-runners
+#js-group-runners{ data: { registration_token: @group.runners_token, group_id: @group.id } }
diff --git a/app/views/layouts/nav/sidebar/_group_menus.html.haml b/app/views/layouts/nav/sidebar/_group_menus.html.haml
index 085e660bae7..5706613c697 100644
--- a/app/views/layouts/nav/sidebar/_group_menus.html.haml
+++ b/app/views/layouts/nav/sidebar/_group_menus.html.haml
@@ -1,20 +1,3 @@
-- merge_requests_count = cached_issuables_count(@group, type: :merge_requests)
-
-- if group_sidebar_link?(:merge_requests)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group) do
- .nav-icon-container
- = sprite_icon('git-merge')
- %span.nav-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count= merge_requests_count
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
- = link_to merge_requests_group_path(@group) do
- %strong.fly-out-top-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count
-
= render_if_exists "layouts/nav/ee/security_link" # EE-specific
= render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
diff --git a/app/views/shared/integrations/_tabs.html.haml b/app/views/shared/integrations/_tabs.html.haml
new file mode 100644
index 00000000000..ff97e371374
--- /dev/null
+++ b/app/views/shared/integrations/_tabs.html.haml
@@ -0,0 +1,14 @@
+.tabs.gl-tabs
+ %div
+ %ul.nav.gl-tabs-nav{ role: 'tablist' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link.gl-tab-nav-item{ role: 'tab', href: scoped_edit_integration_path(integration) }
+ = _('Settings')
+
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link.gl-tab-nav-item.gl-tab-nav-item-active.gl-tab-nav-item-active-indigo.active{ role: 'tab', href: scoped_overrides_integration_path(integration) }
+ = s_('Integrations|Projects using custom settings')
+
+ .tab-content.gl-tab-content
+ .tab-pane.active{ role: 'tabpanel' }
+ = yield
diff --git a/app/views/shared/integrations/overrides.html.haml b/app/views/shared/integrations/overrides.html.haml
new file mode 100644
index 00000000000..4d8cc94e967
--- /dev/null
+++ b/app/views/shared/integrations/overrides.html.haml
@@ -0,0 +1,10 @@
+- add_to_breadcrumbs _('Integrations'), scoped_integrations_path
+- breadcrumb_title @integration.title
+- page_title @integration.title, _('Integrations')
+- @content_class = 'limit-container-width' unless fluid_layout
+
+%h3.page-title
+ = @integration.title
+
+= render 'shared/integrations/tabs', integration: @integration do
+ .js-vue-integration-overrides{ data: integration_overrides_data(@integration) }
diff --git a/app/workers/clusters/applications/deactivate_service_worker.rb b/app/workers/clusters/applications/deactivate_service_worker.rb
index 5c77374479b..4c8d21a7c4d 100644
--- a/app/workers/clusters/applications/deactivate_service_worker.rb
+++ b/app/workers/clusters/applications/deactivate_service_worker.rb
@@ -16,9 +16,11 @@ module Clusters
cluster = Clusters::Cluster.find_by_id(cluster_id)
raise cluster_missing_error(integration_name) unless cluster
- integration = ::Project.integration_association_name(integration_name).to_sym
- cluster.all_projects.with_integration(integration).find_each do |project|
- project.public_send(integration).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
+ integration_class = Integration.integration_name_to_model(integration_name)
+ integration_association_name = ::Project.integration_association_name(integration_name).to_sym
+
+ cluster.all_projects.with_integration(integration_class).include_integration(integration_association_name).find_each do |project|
+ project.public_send(integration_association_name).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/config/feature_flags/development/instance_level_integration_overrides.yml b/config/feature_flags/development/instance_level_integration_overrides.yml
new file mode 100644
index 00000000000..f99b85b3c05
--- /dev/null
+++ b/config/feature_flags/development/instance_level_integration_overrides.yml
@@ -0,0 +1,8 @@
+---
+name: instance_level_integration_overrides
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66723
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336750
+milestone: '14.2'
+type: development
+group: group::ecosystem
+default_enabled: false
diff --git a/config/feature_flags/development/vuln_report_new_project_filter.yml b/config/feature_flags/development/vuln_report_new_project_filter.yml
new file mode 100644
index 00000000000..79c2afaca07
--- /dev/null
+++ b/config/feature_flags/development/vuln_report_new_project_filter.yml
@@ -0,0 +1,8 @@
+---
+name: vuln_report_new_project_filter
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55745
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334380
+milestone: '14.2'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index dcad7a86ac9..d7f73354d4c 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -127,6 +127,7 @@ namespace :admin do
resource :application_settings, only: :update do
resources :integrations, only: [:edit, :update] do
member do
+ get :overrides
put :test
post :reset
end
diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md
index 90a41ed3e1c..d3931c0ba80 100644
--- a/doc/administration/geo/replication/object_storage.md
+++ b/doc/administration/geo/replication/object_storage.md
@@ -9,6 +9,11 @@ type: howto
Geo can be used in combination with Object Storage (AWS S3, or other compatible object storage).
+The storage method for files is recorded in the database, and the database is replicated
+from the **primary** Geo site to the **secondary** Geo site, so the **secondary** Geo site
+must match the storage method of the **primary** Geo site.
+Therefore, if the **primary** Geo site uses object storage, the **secondary** Geo site must use it too.
+
Currently, **secondary** sites can use either:
- The same storage bucket as the **primary** site.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e137c69e2c2..379464ee0b9 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -3375,6 +3375,28 @@ Input type: `PipelineRetryInput`
| <a id="mutationpipelineretryerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationpipelineretrypipeline"></a>`pipeline` | [`Pipeline`](#pipeline) | The pipeline after mutation. |
+### `Mutation.projectSetComplianceFramework`
+
+Assign (or unset) a compliance framework to a project.
+
+Input type: `ProjectSetComplianceFrameworkInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationprojectsetcomplianceframeworkclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationprojectsetcomplianceframeworkcomplianceframeworkid"></a>`complianceFrameworkId` | [`ComplianceManagementFrameworkID`](#compliancemanagementframeworkid) | ID of the compliance framework to assign to the project. |
+| <a id="mutationprojectsetcomplianceframeworkprojectid"></a>`projectId` | [`ProjectID!`](#projectid) | ID of the project to change the compliance framework of. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationprojectsetcomplianceframeworkclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationprojectsetcomplianceframeworkerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationprojectsetcomplianceframeworkproject"></a>`project` | [`Project`](#project) | Project after mutation. |
+
### `Mutation.prometheusIntegrationCreate`
Input type: `PrometheusIntegrationCreateInput`
diff --git a/doc/architecture/blueprints/database_scaling/size-limits.md b/doc/architecture/blueprints/database_scaling/size-limits.md
new file mode 100644
index 00000000000..3f913391f66
--- /dev/null
+++ b/doc/architecture/blueprints/database_scaling/size-limits.md
@@ -0,0 +1,176 @@
+---
+comments: false
+description: 'Database Scalability / Limit table sizes'
+group: database
+---
+
+# Database Scalability: Limit on-disk table size to < 100 GB for GitLab.com
+
+This document is a proposal to work towards reducing and limiting table sizes on GitLab.com. We establish a **measurable target** by limiting table size to a certain threshold. This will be used as an indicator to drive database focus and decision making. With GitLab.com growing, we continuously re-evaluate which tables need to be worked on to prevent or otherwise fix violations.
+
+Note that this is not meant to be a hard rule but rather a strong indication that work needs to be done to break a table apart or otherwise reduce its size.
+
+This is meant to be read in context with the [Database Sharding blueprint](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64115),
+which paints the bigger picture. This proposal here is thought to be part of the "debloating step" below, as we aim to reduce storage requirements and improve data modeling. Partitioning is part of the standard tool-belt: where possible, we can already use partitioning as a solution to cut physical table sizes significantly. Both will help to prepare efforts like decomposition (database usage is already optimized) and sharding (database is already partitioned along an identified data access dimension).
+
+```mermaid
+graph LR
+ Fe(Pick feature) --> D
+ D[(Database)] --> De
+ De[Debloating] --> Dc
+ Dc[Decomposition] --> P
+ P[Partitioning] --> S
+ S[Sharding] --> R
+ P --> M
+ M[Microservices] --> R
+ R{Repeat?} --> Fe
+ style De fill:#fca326
+ style P fill:#fc6d26
+```
+
+## Motivation: GitLab.com stability and performance
+
+Large tables on GitLab.com are a major problem - for both operations and development. They cause a variety of problems:
+
+1. **Query timings** and hence overall application performance suffers
+1. **Table maintenance** becomes much more costly. Vacuum activity has become a significant concern on GitLab.com - with large tables only seeing infrequent (e.g. once per day) and vacuum runs taking many hours to complete. This has various negative consequences and a very large table has potential to impact seemingly unrelated parts of the database and hence overall application performance suffers.
+1. **Data migrations** on large tables are significantly more complex to implement and incur development overhead. They have potential to cause stability problems on GitLab.com and take a long time to execute on large datasets.
+1. **Indexes size** is significant. This directly impacts performance as smaller parts of the index are kept in memory and also makes the indexes harder to maintain (think repacking).
+1. **Index creation times** go up significantly - in 2021, we see btree creation take up to 6 hours for a single btree index. This impacts our ability to deploy frequently and leads to vacuum-related problems (delayed cleanup).
+1. We tend to add **many indexes** to mitigate, but this eventually causes significant overhead, can confuse the query planner and a large number of indexes is a smell of a design problem.
+
+## Examples
+
+Most prominently, the `ci_builds` table is 1.5 TB in size as of June 2021 and has 31 indexes associated with it which sum up to 1 TB in size. The overall on-disk size for this table is 2.5 TB. Currently, this grows at 300 GB per month. By the end of the year, this is thought to be close to 5 TB if we don't take measures against.
+
+The following examples show that very large tables often constitute the root cause of incidents on GitLab.com.
+
+1. Infrequent and long running vacuum activity has led to [repeated degradation of query performance for CI queuing](https://gitlab.com/gitlab-com/gl-infra/production/-/issues?label_name%5B%5D=Service%3A%3ACI+Runners&label_name%5B%5D=incident&scope=all&search=shared_runner_queues&state=all)
+1. On large tables like `ci_builds`, index creation time varies between 1.5 to 6 hours during busy times. This process blocks deployments as migrations are being run synchronously - reducing our ability to deploy frequently.
+1. Creating a large index can lead to a burst of activity on the database primary:
+ 1. on `merge_request_diff_commits` table: caused [high network saturation](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4823),
+ 1. regular reindexing activity on the weekend: causes [growing WAL queue](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4767) (impacts recovery objectives),
+ 1. `notes` table: Re-creating a GIN trigram index for maintenance reasons has become nearly unfeasible and had to be [aborted after 12 hours upon first try](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4633) as it was blocking other vacuum operation.
+
+## Problematic tables on GitLab.com
+
+This shows the TOP30 tables by their total size (includes index sizes) as of mid June 2021 on GitLab.com. `table_size, index_size` is the on-disk size of the actual data and associated indexes, respectively. `percentage_of_total_database_size` displays the ratio of total table size to database size.
+
+As we can see, there are currently very large tables greater than 1 TB in size, which also tend to have very large indexes.
+
+The other observation here is that there are also tables with a large number of indexes and total index size can be significantly larger than the data stored. For example, `deployments` is 30 GB in size plus additional 123 GB of index data spread across 24 indexes.
+
+<!--
+select tablename,
+ pg_size_pretty(pg_total_relation_size(t.schemaname || '.' || t.tablename)) as total_size,
+ pg_size_pretty(pg_relation_size(t.schemaname || '.' || t.tablename)) as table_size,
+ pg_size_pretty(pg_indexes_size(t.schemaname || '.' || t.tablename)) as index_size,
+ count(*) as index_count,
+ round(pg_total_relation_size(t.schemaname || '.' || t.tablename) / pg_database_size('gitlabhq_production')::numeric * 100, 1) as percentage_of_total_database_size
+from pg_indexes i
+join pg_tables t USING (tablename)
+group by 1,
+ 2,
+ 3,
+ t.schemaname,
+ t.tablename
+order by pg_total_relation_size(t.schemaname || '.' || t.tablename) desc
+limit 30;
+-->
+
+| Table | Total size | Table size | Index size | Index count | Percentage of total database size |
+|------------------------------|------------|------------|------------|-------------|-----------------------------------|
+| `ci_builds` | 2975 GB | 1551 GB | 941 GB | 30 | 22.7 |
+| `merge_request_diff_commits` | 1890 GB | 1454 GB | 414 GB | 2 | 14.4 |
+| `ci_build_trace_sections` | 1123 GB | 542 GB | 581 GB | 3 | 8.6 |
+| `notes` | 748 GB | 390 GB | 332 GB | 13 | 5.7 |
+| `merge_request_diff_files` | 575 GB | 481 GB | 88 GB | 1 | 4.4 |
+| `events` | 441 GB | 95 GB | 346 GB | 12 | 3.4 |
+| `ci_job_artifacts` | 397 GB | 187 GB | 210 GB | 10 | 3.0 |
+| `ci_pipelines` | 266 GB | 66 GB | 200 GB | 23 | 2.0 |
+| `taggings` | 238 GB | 60 GB | 179 GB | 5 | 1.8 |
+| `ci_builds_metadata` | 237 GB | 88 GB | 149 GB | 5 | 1.8 |
+| `issues` | 219 GB | 47 GB | 150 GB | 28 | 1.7 |
+| `web_hook_logs_202103` | 186 GB | 122 GB | 8416 MB | 3 | 1.4 |
+| `ci_stages` | 182 GB | 58 GB | 124 GB | 6 | 1.4 |
+| `web_hook_logs_202105` | 180 GB | 115 GB | 7868 MB | 3 | 1.4 |
+| `merge_requests` | 176 GB | 44 GB | 125 GB | 36 | 1.3 |
+| `web_hook_logs_202104` | 176 GB | 115 GB | 7472 MB | 3 | 1.3 |
+| `web_hook_logs_202101` | 169 GB | 112 GB | 7231 MB | 3 | 1.3 |
+| `web_hook_logs_202102` | 167 GB | 111 GB | 7106 MB | 3 | 1.3 |
+| `sent_notifications` | 166 GB | 88 GB | 79 GB | 3 | 1.3 |
+| `web_hook_logs_202011` | 163 GB | 113 GB | 7125 MB | 3 | 1.2 |
+| `push_event_payloads` | 162 GB | 114 GB | 48 GB | 1 | 1.2 |
+| `web_hook_logs_202012` | 159 GB | 106 GB | 6771 MB | 3 | 1.2 |
+| `web_hook_logs_202106` | 156 GB | 101 GB | 6752 MB | 3 | 1.2 |
+| `deployments` | 155 GB | 30 GB | 125 GB | 24 | 1.2 |
+| `web_hook_logs_202010` | 136 GB | 98 GB | 6116 MB | 3 | 1.0 |
+| `web_hook_logs_202009` | 114 GB | 82 GB | 5168 MB | 3 | 0.9 |
+| `security_findings` | 109 GB | 21 GB | 88 GB | 8 | 0.8 |
+| `web_hook_logs_202008` | 92 GB | 66 GB | 3983 MB | 3 | 0.7 |
+| `resource_label_events` | 66 GB | 47 GB | 19 GB | 6 | 0.5 |
+| `merge_request_diffs` | 63 GB | 39 GB | 22 GB | 5 | 0.5 |
+
+## Target: All physical tables on GitLab.com are < 100 GB including indexes
+
+NOTE:
+In PostgreSQL context, a **physical table** is either a regular table or a partition of a partitioned table.
+
+In order to maintain and improve operational stability and lessen development burden, we target a **table size less than 100 GB for a physical table on GitLab.com** (including its indexes). This has numerous benefits:
+
+1. Improved query performance and more stable query plans
+1. Significantly reduce vacuum run times and increase frequency of vacuum runs to maintain a healthy state - reducing overhead on the database primary
+1. Index creation times are significantly faster (significantly less data to read per index)
+1. Indexes are smaller, can be maintained more efficiently and fit better into memory
+1. Data migrations are easier to reason about, take less time to implement and execute
+
+Note that this target is *pragmatic*: We understand table sizes depend on feature usage, code changes and other factors - which all change over time. We may not always find solutions where we can tightly limit the size of physical tables once and for all. That is acceptable though and we primarily aim to keep the situation on GitLab.com under control. We adapt our efforts to the situation present on GitLab.com and will re-evaluate frequently.
+
+While there are changes we can make that lead to a constant maximum physical table size over time, this doesn't need to be the case necessarily. Consider for example hash partitioniong, which breaks a table down into a static number of partitions. With data growth over time, individual partitions will also grow in size and may eventually reach the threshold size again. We strive to get constant table sizes, but it is acceptable to ship easier solutions that don't have this characteristic but improve the situation for a considerable amount of time.
+
+As such, the target size of a physical table after refactoring depends on the situation and there is no hard rule for it. We suggest to consider historic data growth and forecast when physical tables will reach the threshold of 100 GB again. This allows us to understand how long a particular solution is expected to last until the model has to be revisited.
+
+## Solutions
+
+There is no standard solution to reduce table sizes - there are many!
+
+1. **Retention**: Delete unnecessary data, for example expire old and unneeded records.
+1. **Remove STI**: We still use [single-table inheritance](../../../development/single_table_inheritance.md) in a few places, which is considered an anti-pattern. Redesigning this, we can split data into multiple tables.
+1. **Index optimization**: Drop unnecessary indexes and consolidate overlapping indexes if possible.
+1. **Optimise data types**: Review data type decisions and optimise data types where possible (example: use integer instead of text for an enum column)
+1. **Partitioning**: Apply a partitioning scheme if there is a common access dimension.
+1. **Normalization**: Review relational modeling and apply normalization techniques to remove duplicate data
+1. **Vertical table splits**: Review column usage and split table vertically.
+1. **Externalize**: Move large data types out of the database entirely. For example, JSON documents, especially when not used for filtering, may be better stored outside the database, e.g. in object storage.
+
+NOTE:
+While we're targeting to limit physical table sizes, we consider retaining or improving performance a goal, too.
+
+For solutions like normalization, this is a trade-off: Denormalized models can speed up queries when used appropriately, at the expense of table size. When normalizing models, splitting tables or externalizing data, we aim to understand the impact on performance and strive to find a solution to reduce table sizes that doesn't impact performance significantly.
+
+### Example efforts
+
+A few examples can be found below, many more are organized under the epic [Database efficiency](https://gitlab.com/groups/gitlab-org/-/epics/5585).
+
+1. [Reduce number of indexes on `ci_builds`](https://gitlab.com/groups/gitlab-org/-/epics/6203)
+1. [Normalize and de-duplicate committer and author details in merge_request_diff_commits](https://gitlab.com/gitlab-org/gitlab/-/issues/331823)
+1. [Retention strategy for `ci_build_trace_sections`](https://gitlab.com/gitlab-org/gitlab/-/issues/32565#note_603138100)
+1. [Implement worker that hard-deletes old CI jobs metadata](https://gitlab.com/gitlab-org/gitlab/-/issues/215646)
+1. [merge_request_diff_files violates < 100 GB target](https://gitlab.com/groups/gitlab-org/-/epics/6215) (epic)
+
+## Goal
+
+The [epic for `~group::database`](https://gitlab.com/groups/gitlab-org/-/epics/6211) drives decision making to establish and communicate the target and to identify and propose necessary changes to reach it. Those changes should primarily be driven by the respective stage group owning the data (and the feature using it), with `~group::database` to support.
+
+## Who
+
+<!-- vale gitlab.Spelling = NO -->
+
+Identifying solutions for offending tables is driven by the [GitLab Database Team](https://about.gitlab.com/handbook/engineering/development/enablement/database/) and respective stage groups.
+
+| Role | Who
+|------------------------------|-------------------------|
+| Author | Andreas Brandl |
+| Engineering Leader | Craig Gomes |
+
+<!-- vale gitlab.Spelling = YES -->
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index 30d61770094..61993d37bac 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -181,7 +181,9 @@ username = <your_personal_access_token_name>
password = <your_personal_access_token>
```
-- Your project ID is on your project's home page.
+The `<project_id>` is either the project's
+[URL-encoded](../../../api/index.md#namespaced-path-encoding)
+path (for example, `group%2Fproject`), or the project's ID (for example `42`).
### Authenticate with a deploy token
@@ -198,7 +200,9 @@ username = <deploy token username>
password = <deploy token>
```
-Your project ID is on your project's home page.
+The `<project_id>` is either the project's
+[URL-encoded](../../../api/index.md#namespaced-path-encoding)
+path (for example, `group%2Fproject`), or the project's ID (for example `42`).
### Authenticate with a CI job token
@@ -335,7 +339,8 @@ pip install --index-url https://<personal_access_token_name>:<personal_access_to
- `<package_name>` is the package name.
- `<personal_access_token_name>` is a personal access token name with the `read_api` scope.
- `<personal_access_token>` is a personal access token with the `read_api` scope.
-- `<project_id>` is the project ID.
+- `<project_id>` is either the project's [URL-encoded](../../../api/index.md#namespaced-path-encoding)
+ path (for example, `group%2Fproject`), or the project's ID (for example `42`).
In these commands, you can use `--extra-index-url` instead of `--index-url`. However, using
`--extra-index-url` makes you vulnerable to dependency confusion attacks because it checks the PyPi
diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md
index 6cada5648cb..dc55b34d4ba 100644
--- a/doc/user/project/clusters/add_remove_clusters.md
+++ b/doc/user/project/clusters/add_remove_clusters.md
@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/327908) in GitLab 14.0.
WARNING:
-Creating a new cluster or adding an existing cluster to GitLab through the certificate-based method
+Creating a new cluster through the certificate-based method
is deprecated and no longer recommended. Kubernetes cluster, similar to any other
infrastructure, should be created, updated, maintained using [Infrastructure as Code](../../infrastructure/index.md).
GitLab is developing a built-in capability to create clusters with Terraform.
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index 7c5f8bb4d99..8dd1631ebf8 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -40,7 +40,7 @@ module API
end
params do
- requires :id, type: Integer, desc: 'The ID of a group'
+ requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
after_validation do
@@ -96,7 +96,7 @@ module API
end
params do
- requires :id, type: Integer, desc: 'The ID of a project'
+ requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb
index 7fee1d0727f..e4a92ed5122 100644
--- a/lib/gitlab/reactive_cache_set_cache.rb
+++ b/lib/gitlab/reactive_cache_set_cache.rb
@@ -10,11 +10,6 @@ module Gitlab
@expires_in = expires_in
end
- # NOTE Remove as part of #331319
- def old_cache_key(key)
- "#{cache_namespace}:#{key}:set"
- end
-
def cache_key(key)
super(key)
end
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
index 7de53c4b3ff..3061fb96190 100644
--- a/lib/gitlab/repository_set_cache.rb
+++ b/lib/gitlab/repository_set_cache.rb
@@ -13,11 +13,6 @@ module Gitlab
@expires_in = expires_in
end
- # NOTE Remove as part of #331319
- def old_cache_key(type)
- "#{type}:#{namespace}:set"
- end
-
def cache_key(type)
super("#{type}:#{namespace}")
end
diff --git a/lib/gitlab/set_cache.rb b/lib/gitlab/set_cache.rb
index 9fc7a44ec99..feb2c3c1d7d 100644
--- a/lib/gitlab/set_cache.rb
+++ b/lib/gitlab/set_cache.rb
@@ -10,11 +10,6 @@ module Gitlab
@expires_in = expires_in
end
- # NOTE Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/331319
- def old_cache_key(key)
- "#{key}:set"
- end
-
def cache_key(key)
"#{cache_namespace}:#{key}:set"
end
@@ -25,7 +20,6 @@ module Gitlab
with do |redis|
keys_to_expire = keys.map { |key| cache_key(key) }
- keys_to_expire += keys.map { |key| old_cache_key(key) } # NOTE Remove as part of #331319
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.unlink(*keys_to_expire)
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 96cbf9c4808..4802c45c949 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -652,9 +652,9 @@ module Gitlab
todos: distinct_count(::Todo.where(time_period), :author_id),
service_desk_enabled_projects: distinct_count_service_desk_enabled_projects(time_period),
service_desk_issues: count(::Issue.service_desk.where(time_period)),
- projects_jira_active: distinct_count(::Project.with_active_jira_integrations.where(time_period), :creator_id),
- projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_jira_integrations.with_jira_dvcs_cloud.where(time_period), :creator_id),
- projects_jira_dvcs_server_active: distinct_count(::Project.with_active_jira_integrations.with_jira_dvcs_server.where(time_period), :creator_id)
+ projects_jira_active: distinct_count(::Project.with_active_integration(::Integrations::Jira) .where(time_period), :creator_id),
+ projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_integration(::Integrations::Jira) .with_jira_dvcs_cloud.where(time_period), :creator_id),
+ projects_jira_dvcs_server_active: distinct_count(::Project.with_active_integration(::Integrations::Jira) .with_jira_dvcs_server.where(time_period), :creator_id)
}
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/sidebars/groups/menus/merge_requests_menu.rb b/lib/sidebars/groups/menus/merge_requests_menu.rb
new file mode 100644
index 00000000000..7faf50305c6
--- /dev/null
+++ b/lib/sidebars/groups/menus/merge_requests_menu.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Sidebars
+ module Groups
+ module Menus
+ class MergeRequestsMenu < ::Sidebars::Menu
+ include Gitlab::Utils::StrongMemoize
+
+ override :link
+ def link
+ merge_requests_group_path(context.group)
+ end
+
+ override :title
+ def title
+ _('Merge requests')
+ end
+
+ override :sprite_icon
+ def sprite_icon
+ 'git-merge'
+ end
+
+ override :render?
+ def render?
+ can?(context.current_user, :read_group_merge_requests, context.group)
+ end
+
+ override :has_pill?
+ def has_pill?
+ true
+ end
+
+ override :pill_count
+ def pill_count
+ strong_memoize(:pill_count) do
+ count_service = ::Groups::MergeRequestsCountService
+ count = count_service.new(context.group, context.current_user).count
+
+ format_cached_count(count_service, count)
+ end
+ end
+
+ override :pill_html_options
+ def pill_html_options
+ {
+ class: 'merge_counter js-merge-counter'
+ }
+ end
+
+ override :active_routes
+ def active_routes
+ { path: 'groups#merge_requests' }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/groups/panel.rb b/lib/sidebars/groups/panel.rb
index e828905594f..8201ad0f180 100644
--- a/lib/sidebars/groups/panel.rb
+++ b/lib/sidebars/groups/panel.rb
@@ -9,6 +9,7 @@ module Sidebars
add_menu(Sidebars::Groups::Menus::GroupInformationMenu.new(context))
add_menu(Sidebars::Groups::Menus::IssuesMenu.new(context))
+ add_menu(Sidebars::Groups::Menus::MergeRequestsMenu.new(context))
end
override :render_raw_menus_partial
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e7da27582ae..753e559eee1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3963,14 +3963,18 @@ msgstr ""
msgid "ApplicationSettings|Allowed domains for sign-ups"
msgstr ""
-msgid "ApplicationSettings|Approve all users in the pending approval status?"
-msgstr ""
+msgid "ApplicationSettings|Approve %d user"
+msgid_plural "ApplicationSettings|Approve %d users"
+msgstr[0] ""
+msgstr[1] ""
-msgid "ApplicationSettings|Approve users"
+msgid "ApplicationSettings|Approve users in the pending approval status?"
msgstr ""
-msgid "ApplicationSettings|By making this change, you will automatically approve all users in pending approval status."
-msgstr ""
+msgid "ApplicationSettings|By making this change, you will automatically approve %d user with the pending approval status."
+msgid_plural "ApplicationSettings|By making this change, you will automatically approve %d users with the pending approval status."
+msgstr[0] ""
+msgstr[1] ""
msgid "ApplicationSettings|Denied domains for sign-ups"
msgstr ""
@@ -17707,6 +17711,9 @@ msgstr ""
msgid "Integrations|Note: this integration only works with accounts on GitLab.com (SaaS)."
msgstr ""
+msgid "Integrations|Projects using custom settings"
+msgstr ""
+
msgid "Integrations|Projects using custom settings will not be affected."
msgstr ""
@@ -22890,7 +22897,7 @@ msgstr ""
msgid "OnCallSchedules|You are currently a part of:"
msgstr ""
-msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the add a rotation button. To create an escalation policy that defines which schedule is used when, visit the %{linkStart}escalation policy%{linkEnd} page."
+msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the Add a rotation button. To enable notifications for this schedule, you must also create an %{linkStart}escalation policy%{linkEnd}."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
@@ -29288,6 +29295,9 @@ msgstr ""
msgid "SecurityReports|Manage and track vulnerabilities identified in your selected projects. Vulnerabilities for selected projects with security testing configured are shown here."
msgstr ""
+msgid "SecurityReports|Maximum selected projects limit reached"
+msgstr ""
+
msgid "SecurityReports|Monitor vulnerabilities in all of your projects"
msgstr ""
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 5f52d48e9f6..afe88fc0cdc 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -202,7 +202,9 @@ module QA
def has_pipeline_status?(text)
# Pipelines can be slow, so we wait a bit longer than the usual 10 seconds
- has_element?(:merge_request_pipeline_info_content, text: text, wait: 60)
+ wait_until(sleep_interval: 5, reload: false) do
+ has_element?(:merge_request_pipeline_info_content, text: text, wait: 15 )
+ end
end
def has_title?(title)
@@ -236,7 +238,10 @@ module QA
end
def merged?
- # Revisit after merge page re-architect is done https://gitlab.com/gitlab-org/gitlab/-/issues/300042
+ # Reloads the page at this point to avoid the problem of the merge status failing to update
+ # That's the transient UX issue this test is checking for, so if the MR is merged but the UI still shows the
+ # status as unmerged, the test will fail.
+ # Revisit after merge page re-architect is done https://gitlab.com/groups/gitlab-org/-/epics/5598
# To remove page refresh logic if possible
retry_until(max_attempts: 3, reload: true) do
has_element?(:merged_status_content, text: 'The changes were merged into', wait: 20)
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb
index 01aada2d6dd..b43581289ef 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb
@@ -73,7 +73,7 @@ module QA
it 'can still merge MR successfully', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/971' do
Page::MergeRequest::Show.perform do |show|
# waiting for manual action status shows status badge 'blocked' on pipelines page
- show.wait_until(reload: false) { show.has_pipeline_status?('waiting for manual action') }
+ show.has_pipeline_status?('waiting for manual action')
show.merge!
expect(show).to be_merged
diff --git a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
index 07484feb686..7d3f8f2b1d4 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
@@ -39,9 +39,7 @@ module QA
merge_request.visit!
Page::MergeRequest::Show.perform do |mr_widget|
- Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 5) do
- mr_widget.has_pipeline_status?('passed')
- end
+ mr_widget.has_pipeline_status?('passed')
expect(mr_widget).to have_content('Test coverage 66.67%')
end
end
diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb
index 0d68cd5a64d..3d37fe7bf79 100644
--- a/spec/controllers/admin/integrations_controller_spec.rb
+++ b/spec/controllers/admin/integrations_controller_spec.rb
@@ -97,4 +97,52 @@ RSpec.describe Admin::IntegrationsController do
.and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end
end
+
+ describe '#overrides' do
+ let_it_be(:instance_integration) { create(:bugzilla_integration, :instance) }
+ let_it_be(:non_overridden_integration) { create(:bugzilla_integration, inherit_from_id: instance_integration.id) }
+ let_it_be(:overridden_integration) { create(:bugzilla_integration) }
+ let_it_be(:overridden_other_integration) { create(:confluence_integration) }
+
+ subject do
+ get :overrides, params: { id: instance_integration.class.to_param }, format: format
+ end
+
+ context 'when format is JSON' do
+ let(:format) { :json }
+
+ include_context 'JSON response'
+
+ it 'returns projects with overrides', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to contain_exactly(a_hash_including('full_name' => overridden_integration.project.full_name))
+ end
+ end
+
+ context 'when format is HTML' do
+ let(:format) { :html }
+
+ it 'renders template' do
+ subject
+
+ expect(response).to render_template 'shared/integrations/overrides'
+ expect(assigns(:integration)).to eq(instance_integration)
+ end
+
+ context 'when `instance_level_integration_overrides` is not enabled' do
+ before do
+ stub_feature_flags(instance_level_integration_overrides: false)
+ end
+
+ it 'renders a 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
end
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
index 18339164d5a..4bb22feb913 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -192,22 +192,27 @@ describe('Signup Form', () => {
describe('form submit button confirmation modal for side-effect of adding possibly unwanted new users', () => {
it.each`
- requireAdminApprovalAction | userCapAction | buttonEffect
- ${'unchanged from true'} | ${'unchanged'} | ${'submits form'}
- ${'unchanged from false'} | ${'unchanged'} | ${'submits form'}
- ${'toggled off'} | ${'unchanged'} | ${'shows confirmation modal'}
- ${'toggled on'} | ${'unchanged'} | ${'submits form'}
- ${'unchanged from false'} | ${'increased'} | ${'shows confirmation modal'}
- ${'unchanged from true'} | ${'increased'} | ${'shows confirmation modal'}
- ${'toggled off'} | ${'increased'} | ${'shows confirmation modal'}
- ${'toggled on'} | ${'increased'} | ${'shows confirmation modal'}
- ${'toggled on'} | ${'decreased'} | ${'submits form'}
- ${'unchanged from false'} | ${'changed from limited to unlimited'} | ${'shows confirmation modal'}
- ${'unchanged from false'} | ${'changed from unlimited to limited'} | ${'submits form'}
- ${'unchanged from false'} | ${'unchanged from unlimited'} | ${'submits form'}
+ requireAdminApprovalAction | userCapAction | pendingUserCount | buttonEffect
+ ${'unchanged from true'} | ${'unchanged'} | ${0} | ${'submits form'}
+ ${'unchanged from false'} | ${'unchanged'} | ${0} | ${'submits form'}
+ ${'toggled off'} | ${'unchanged'} | ${1} | ${'shows confirmation modal'}
+ ${'toggled off'} | ${'unchanged'} | ${0} | ${'submits form'}
+ ${'toggled on'} | ${'unchanged'} | ${0} | ${'submits form'}
+ ${'unchanged from false'} | ${'increased'} | ${1} | ${'shows confirmation modal'}
+ ${'unchanged from true'} | ${'increased'} | ${0} | ${'submits form'}
+ ${'toggled off'} | ${'increased'} | ${1} | ${'shows confirmation modal'}
+ ${'toggled off'} | ${'increased'} | ${0} | ${'submits form'}
+ ${'toggled on'} | ${'increased'} | ${1} | ${'shows confirmation modal'}
+ ${'toggled on'} | ${'increased'} | ${0} | ${'submits form'}
+ ${'toggled on'} | ${'decreased'} | ${0} | ${'submits form'}
+ ${'toggled on'} | ${'decreased'} | ${1} | ${'submits form'}
+ ${'unchanged from false'} | ${'changed from limited to unlimited'} | ${1} | ${'shows confirmation modal'}
+ ${'unchanged from false'} | ${'changed from limited to unlimited'} | ${0} | ${'submits form'}
+ ${'unchanged from false'} | ${'changed from unlimited to limited'} | ${0} | ${'submits form'}
+ ${'unchanged from false'} | ${'unchanged from unlimited'} | ${0} | ${'submits form'}
`(
- '$buttonEffect if require admin approval for new sign-ups is $requireAdminApprovalAction and the user cap is $userCapAction',
- async ({ requireAdminApprovalAction, userCapAction, buttonEffect }) => {
+ '$buttonEffect if require admin approval for new sign-ups is $requireAdminApprovalAction and the user cap is $userCapAction and pending user count is $pendingUserCount',
+ async ({ requireAdminApprovalAction, userCapAction, pendingUserCount, buttonEffect }) => {
let isModalDisplayed;
switch (buttonEffect) {
@@ -224,7 +229,9 @@ describe('Signup Form', () => {
const isFormSubmittedWhenClickingFormSubmitButton = !isModalDisplayed;
- const injectedProps = {};
+ const injectedProps = {
+ pendingUserCount,
+ };
const USER_CAP_DEFAULT = 5;
@@ -310,6 +317,7 @@ describe('Signup Form', () => {
await mountComponent({
injectedProps: {
newUserSignupsCap: INITIAL_USER_CAP,
+ pendingUserCount: 5,
},
stubs: { GlButton, GlModal: stubComponent(GlModal) },
});
diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js
index 624a5614c9c..135fc8caae0 100644
--- a/spec/frontend/admin/signup_restrictions/mock_data.js
+++ b/spec/frontend/admin/signup_restrictions/mock_data.js
@@ -17,6 +17,7 @@ export const rawMockData = {
supportedSyntaxLinkUrl: '/supported/syntax/link',
emailRestrictions: 'user1@domain.com, user2@domain.com',
afterSignUpText: 'Congratulations on your successful sign-up!',
+ pendingUserCount: '0',
};
export const mockData = {
@@ -38,4 +39,5 @@ export const mockData = {
supportedSyntaxLinkUrl: '/supported/syntax/link',
emailRestrictions: 'user1@domain.com, user2@domain.com',
afterSignUpText: 'Congratulations on your successful sign-up!',
+ pendingUserCount: '0',
};
diff --git a/spec/frontend/runner/components/runner_registration_token_reset_spec.js b/spec/frontend/runner/components/runner_registration_token_reset_spec.js
index 6dc207e369c..8b360b88417 100644
--- a/spec/frontend/runner/components/runner_registration_token_reset_spec.js
+++ b/spec/frontend/runner/components/runner_registration_token_reset_spec.js
@@ -1,11 +1,12 @@
import { GlButton } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash, { FLASH_TYPES } from '~/flash';
import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
-import { INSTANCE_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
@@ -23,11 +24,13 @@ describe('RunnerRegistrationTokenReset', () => {
const findButton = () => wrapper.findComponent(GlButton);
- const createComponent = () => {
+ const createComponent = ({ props, provide = {} } = {}) => {
wrapper = shallowMount(RunnerRegistrationTokenReset, {
localVue,
+ provide,
propsData: {
type: INSTANCE_TYPE,
+ ...props,
},
apolloProvider: createMockApollo([
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
@@ -59,31 +62,47 @@ describe('RunnerRegistrationTokenReset', () => {
});
describe('On click and confirmation', () => {
- beforeEach(async () => {
- window.confirm.mockReturnValueOnce(true);
- await findButton().vm.$emit('click');
- });
+ const mockGroupId = '11';
+ const mockProjectId = '22';
+
+ describe.each`
+ type | provide | expectedInput
+ ${INSTANCE_TYPE} | ${{}} | ${{ type: INSTANCE_TYPE }}
+ ${GROUP_TYPE} | ${{ groupId: mockGroupId }} | ${{ type: GROUP_TYPE, id: `gid://gitlab/Group/${mockGroupId}` }}
+ ${PROJECT_TYPE} | ${{ projectId: mockProjectId }} | ${{ type: PROJECT_TYPE, id: `gid://gitlab/Project/${mockProjectId}` }}
+ `('Resets token of type $type', ({ type, provide, expectedInput }) => {
+ beforeEach(async () => {
+ createComponent({
+ provide,
+ props: { type },
+ });
+
+ window.confirm.mockReturnValueOnce(true);
+ findButton().vm.$emit('click');
+ await waitForPromises();
+ });
- it('resets token', () => {
- expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledTimes(1);
- expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledWith({
- input: { type: INSTANCE_TYPE },
+ it('resets token', () => {
+ expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledTimes(1);
+ expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledWith({
+ input: expectedInput,
+ });
});
- });
- it('emits result', () => {
- expect(wrapper.emitted('tokenReset')).toHaveLength(1);
- expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]);
- });
+ it('emits result', () => {
+ expect(wrapper.emitted('tokenReset')).toHaveLength(1);
+ expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]);
+ });
- it('does not show a loading state', () => {
- expect(findButton().props('loading')).toBe(false);
- });
+ it('does not show a loading state', () => {
+ expect(findButton().props('loading')).toBe(false);
+ });
- it('shows confirmation', () => {
- expect(createFlash).toHaveBeenLastCalledWith({
- message: expect.stringContaining('registration token generated'),
- type: FLASH_TYPES.SUCCESS,
+ it('shows confirmation', () => {
+ expect(createFlash).toHaveBeenLastCalledWith({
+ message: expect.stringContaining('registration token generated'),
+ type: FLASH_TYPES.SUCCESS,
+ });
});
});
});
@@ -91,7 +110,8 @@ describe('RunnerRegistrationTokenReset', () => {
describe('On click without confirmation', () => {
beforeEach(async () => {
window.confirm.mockReturnValueOnce(false);
- await findButton().vm.$emit('click');
+ findButton().vm.$emit('click');
+ await waitForPromises();
});
it('does not reset token', () => {
@@ -118,7 +138,7 @@ describe('RunnerRegistrationTokenReset', () => {
runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
window.confirm.mockReturnValueOnce(true);
- await findButton().vm.$emit('click');
+ findButton().vm.$emit('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -144,7 +164,7 @@ describe('RunnerRegistrationTokenReset', () => {
});
window.confirm.mockReturnValueOnce(true);
- await findButton().vm.$emit('click');
+ findButton().vm.$emit('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
@@ -160,7 +180,8 @@ describe('RunnerRegistrationTokenReset', () => {
describe('Immediately after click', () => {
it('shows loading state', async () => {
window.confirm.mockReturnValue(true);
- await findButton().vm.$emit('click');
+ findButton().vm.$emit('click');
+ await nextTick();
expect(findButton().props('loading')).toBe(true);
});
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 06329686a00..6a0863e92b4 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -1,14 +1,22 @@
import { shallowMount } from '@vue/test-utils';
+import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
+const mockRegistrationToken = 'AABBCC';
+
describe('GroupRunnersApp', () => {
let wrapper;
const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
+ const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
const createComponent = ({ mountFn = shallowMount } = {}) => {
- wrapper = mountFn(GroupRunnersApp);
+ wrapper = mountFn(GroupRunnersApp, {
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ },
+ });
};
beforeEach(() => {
@@ -18,4 +26,9 @@ describe('GroupRunnersApp', () => {
it('shows the runner type help', () => {
expect(findRunnerTypeHelp().exists()).toBe(true);
});
+
+ it('shows the runner setup instructions', () => {
+ expect(findRunnerManualSetupHelp().exists()).toBe(true);
+ expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ });
});
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 13f84fecaa5..5703bfeaea7 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -554,23 +554,4 @@ RSpec.describe GroupsHelper do
expect(helper.render_setting_to_allow_project_access_token_creation?(group)).to be_falsy
end
end
-
- describe '#cached_issuables_count' do
- let_it_be(:current_user) { create(:user) }
- let_it_be(:group) { create(:group, name: 'group') }
-
- context 'with issues type' do
- let(:type) { :issues }
- let(:count_service) { Groups::OpenIssuesCountService }
-
- it_behaves_like 'cached issuables count'
- end
-
- context 'with merge requests type' do
- let(:type) { :merge_requests }
- let(:count_service) { Groups::MergeRequestsCountService }
-
- it_behaves_like 'cached issuables count'
- end
- end
end
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index 4dcf9dc2c05..c93fd884347 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -94,12 +94,6 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
expect(cache.read(:foo)).to be_empty
end
-
- it 'expires the old key format' do
- expect_any_instance_of(Redis).to receive(:unlink).with(cache.cache_key(:foo), cache.old_cache_key(:foo)) # rubocop:disable RSpec/AnyInstanceOf
-
- subject
- end
end
context 'multiple keys' do
diff --git a/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb b/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb
new file mode 100644
index 00000000000..3aceff29d6d
--- /dev/null
+++ b/spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Groups::Menus::MergeRequestsMenu do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) do
+ build(:group, :private).tap do |g|
+ g.add_owner(owner)
+ end
+ end
+
+ let(:user) { owner }
+ let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
+ let(:menu) { described_class.new(context) }
+
+ describe '#render?' do
+ context 'when user can read merge requests' do
+ it 'returns true' do
+ expect(menu.render?).to eq true
+ end
+ end
+
+ context 'when user cannot read merge requests' do
+ let(:user) { nil }
+
+ it 'returns false' do
+ expect(menu.render?).to eq false
+ end
+ end
+ end
+
+ it_behaves_like 'pill_count formatted results' do
+ let(:count_service) { ::Groups::MergeRequestsCountService }
+ end
+end
diff --git a/spec/models/concerns/has_integrations_spec.rb b/spec/models/concerns/has_integrations_spec.rb
index 6b3f75bfcfd..ea6b0e69209 100644
--- a/spec/models/concerns/has_integrations_spec.rb
+++ b/spec/models/concerns/has_integrations_spec.rb
@@ -17,14 +17,6 @@ RSpec.describe HasIntegrations do
create(:integrations_slack, project: project_4, inherit_from_id: nil)
end
- describe '.with_custom_integration_for' do
- it 'returns projects with custom integrations' do
- # We use pagination to verify that the group is excluded from the query
- expect(Project.with_custom_integration_for(instance_integration, 0, 2)).to contain_exactly(project_2, project_3)
- expect(Project.with_custom_integration_for(instance_integration)).to contain_exactly(project_2, project_3)
- end
- end
-
describe '.without_integration' do
it 'returns projects without integration' do
expect(Project.without_integration(instance_integration)).to contain_exactly(project_4)
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 96af9690b39..16d3dc086ea 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -61,6 +61,24 @@ RSpec.describe Integration do
end
describe 'Scopes' do
+ describe '.inherit' do
+ it 'returns the correct integrations' do
+ instance_integration = create(:integration, :instance)
+ inheriting_integration = create(:integration, inherit_from_id: instance_integration.id)
+
+ expect(described_class.inherit).to match_array([inheriting_integration])
+ end
+ end
+
+ describe '.not_inherited' do
+ it 'returns the correct integrations' do
+ instance_integration = create(:integration, :instance)
+ create(:integration, inherit_from_id: instance_integration.id)
+
+ expect(described_class.not_inherited).to match_array([instance_integration])
+ end
+ end
+
describe '.by_type' do
let!(:service1) { create(:jira_integration) }
let!(:service2) { create(:jira_integration) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 777f4f52798..9ce96e7d015 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Issue do
it { is_expected.to have_and_belong_to_many(:self_managed_prometheus_alert_events) }
it { is_expected.to have_many(:prometheus_alerts) }
it { is_expected.to have_many(:issue_email_participants) }
+ it { is_expected.to have_many(:timelogs).autosave(true) }
describe 'versions.most_recent' do
it 'returns the most recent version' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 7d0fa9fb95b..ec14e6d442c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1486,33 +1486,21 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '.with_active_jira_integrations' do
- it 'returns the correct integrations' do
- active_jira_integration = create(:jira_integration)
- active_service = create(:service, active: true)
-
- expect(described_class.with_active_jira_integrations).to include(active_jira_integration.project)
- expect(described_class.with_active_jira_integrations).not_to include(active_service.project)
- end
- end
-
describe '.with_jira_dvcs_cloud' do
it 'returns the correct project' do
jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud)
- jira_dvcs_server_project = create(:project, :jira_dvcs_server)
+ create(:project, :jira_dvcs_server)
- expect(described_class.with_jira_dvcs_cloud).to include(jira_dvcs_cloud_project)
- expect(described_class.with_jira_dvcs_cloud).not_to include(jira_dvcs_server_project)
+ expect(described_class.with_jira_dvcs_cloud).to contain_exactly(jira_dvcs_cloud_project)
end
end
describe '.with_jira_dvcs_server' do
it 'returns the correct project' do
jira_dvcs_server_project = create(:project, :jira_dvcs_server)
- jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud)
+ create(:project, :jira_dvcs_cloud)
- expect(described_class.with_jira_dvcs_server).to include(jira_dvcs_server_project)
- expect(described_class.with_jira_dvcs_server).not_to include(jira_dvcs_cloud_project)
+ expect(described_class.with_jira_dvcs_server).to contain_exactly(jira_dvcs_server_project)
end
end
@@ -1598,15 +1586,39 @@ RSpec.describe Project, factory_default: :keep do
end
describe '.with_integration' do
- before do
- create_list(:prometheus_project, 2)
+ it 'returns the correct projects' do
+ active_confluence_integration = create(:confluence_integration)
+ inactive_confluence_integration = create(:confluence_integration, active: false)
+ create(:bugzilla_integration)
+
+ expect(described_class.with_integration(::Integrations::Confluence)).to contain_exactly(
+ active_confluence_integration.project,
+ inactive_confluence_integration.project
+ )
end
+ end
+
+ describe '.with_active_integration' do
+ it 'returns the correct projects' do
+ active_confluence_integration = create(:confluence_integration)
+ create(:confluence_integration, active: false)
+ create(:bugzilla_integration, active: true)
+
+ expect(described_class.with_active_integration(::Integrations::Confluence)).to contain_exactly(
+ active_confluence_integration.project
+ )
+ end
+ end
- let(:integration) { :prometheus_integration }
+ describe '.include_integration' do
+ it 'avoids n + 1', :aggregate_failures do
+ create(:prometheus_integration)
+ run_test = -> { described_class.include_integration(:prometheus_integration).map(&:prometheus_integration) }
+ control_count = ActiveRecord::QueryRecorder.new { run_test.call }
+ create(:prometheus_integration)
- it 'avoids n + 1' do
- expect { described_class.with_integration(integration).map(&integration) }
- .not_to exceed_query_limit(1)
+ expect(run_test.call.count).to eq(2)
+ expect { run_test.call }.not_to exceed_query_limit(control_count)
end
end
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index e66326db2a2..1ade6e91134 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -38,6 +38,12 @@ RSpec.describe API::PypiPackages do
end
it_behaves_like 'deploy token for package GET requests'
+
+ context 'with group path as id' do
+ let(:url) { "/groups/#{CGI.escape(group.full_path)}/-/packages/pypi/simple/#{package.name}" }
+
+ it_behaves_like 'deploy token for package GET requests'
+ end
end
context 'job token' do
@@ -61,6 +67,12 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'rejects PyPI access with unknown project id'
it_behaves_like 'deploy token for package GET requests'
it_behaves_like 'job token for package GET requests'
+
+ context 'with project path as id' do
+ let(:url) { "/projects/#{CGI.escape(project.full_path)}/packages/pypi/simple/#{package.name}" }
+
+ it_behaves_like 'deploy token for package GET requests'
+ end
end
end
diff --git a/spec/serializers/integrations/project_entity_spec.rb b/spec/serializers/integrations/project_entity_spec.rb
new file mode 100644
index 00000000000..1564f7fad63
--- /dev/null
+++ b/spec/serializers/integrations/project_entity_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ProjectEntity do
+ let_it_be(:project) { create(:project, :with_avatar) }
+
+ let(:entity) do
+ described_class.new(project)
+ end
+
+ context 'as json' do
+ include Gitlab::Routing.url_helpers
+
+ subject { entity.as_json }
+
+ it 'contains needed attributes' do
+ expect(subject).to include(
+ avatar_url: include('uploads'),
+ name: project.name,
+ full_path: project_path(project),
+ full_name: project.full_name
+ )
+ end
+ end
+end
diff --git a/spec/serializers/integrations/project_serializer_spec.rb b/spec/serializers/integrations/project_serializer_spec.rb
new file mode 100644
index 00000000000..053548075bb
--- /dev/null
+++ b/spec/serializers/integrations/project_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ProjectSerializer do
+ it 'represents Integrations::ProjectEntity entities' do
+ expect(described_class.entity_class).to eq(Integrations::ProjectEntity)
+ end
+end
diff --git a/spec/services/packages/debian/generate_distribution_service_spec.rb b/spec/services/packages/debian/generate_distribution_service_spec.rb
index a162e492e7e..7da14ac2a18 100644
--- a/spec/services/packages/debian/generate_distribution_service_spec.rb
+++ b/spec/services/packages/debian/generate_distribution_service_spec.rb
@@ -12,13 +12,7 @@ RSpec.describe Packages::Debian::GenerateDistributionService do
context "for #{container_type}" do
include_context 'with Debian distribution', container_type
- context 'with Debian components and architectures' do
- it_behaves_like 'Generate Debian Distribution and component files'
- end
-
- context 'without components and architectures' do
- it_behaves_like 'Generate minimal Debian Distribution'
- end
+ it_behaves_like 'Generate Debian Distribution and component files'
end
end
end
diff --git a/spec/services/packages/debian/sign_distribution_service_spec.rb b/spec/services/packages/debian/sign_distribution_service_spec.rb
index b1096ba235f..2aec0e50636 100644
--- a/spec/services/packages/debian/sign_distribution_service_spec.rb
+++ b/spec/services/packages/debian/sign_distribution_service_spec.rb
@@ -6,12 +6,11 @@ RSpec.describe Packages::Debian::SignDistributionService do
let_it_be(:group) { create(:group, :public) }
let(:content) { FFaker::Lorem.paragraph }
- let(:params) { {} }
- let(:service) { described_class.new(distribution, content, params: params) }
+ let(:service) { described_class.new(distribution, content, detach: detach) }
shared_examples 'Sign Distribution' do |container_type, detach: false|
context "for #{container_type} detach=#{detach}" do
- let(:params) { { detach: detach } }
+ let(:detach) { detach }
if container_type == :group
let_it_be(:distribution) { create('debian_group_distribution', container: group) }
diff --git a/spec/support/shared_examples/helpers/groups_shared_examples.rb b/spec/support/shared_examples/helpers/groups_shared_examples.rb
deleted file mode 100644
index 9c74d25b31f..00000000000
--- a/spec/support/shared_examples/helpers/groups_shared_examples.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-# This shared_example requires the following variables:
-# - current_user
-# - group
-# - type, the issuable type (ie :issues, :merge_requests)
-# - count_service, the Service used by the specified issuable type
-
-RSpec.shared_examples 'cached issuables count' do
- subject { helper.cached_issuables_count(group, type: type) }
-
- before do
- allow(helper).to receive(:current_user) { current_user }
- allow(count_service).to receive(:new).and_call_original
- end
-
- it 'calls the correct service class' do
- subject
- expect(count_service).to have_received(:new).with(group, current_user)
- end
-
- it 'returns all digits for count value under 1000' do
- allow_next_instance_of(count_service) do |service|
- allow(service).to receive(:count).and_return(999)
- end
-
- expect(subject).to eq('999')
- end
-
- it 'returns truncated digits for count value over 1000' do
- allow_next_instance_of(count_service) do |service|
- allow(service).to receive(:count).and_return(2300)
- end
-
- expect(subject).to eq('2.3k')
- end
-
- it 'returns truncated digits for count value over 10000' do
- allow_next_instance_of(count_service) do |service|
- allow(service).to receive(:count).and_return(12560)
- end
-
- expect(subject).to eq('12.6k')
- end
-
- it 'returns truncated digits for count value over 100000' do
- allow_next_instance_of(count_service) do |service|
- allow(service).to receive(:count).and_return(112560)
- end
-
- expect(subject).to eq('112.6k')
- end
-end
diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
index 10625862668..17daa183a7e 100644
--- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
@@ -1,167 +1,181 @@
# frozen_string_literal: true
RSpec.shared_examples 'Generate Debian Distribution and component files' do
- let_it_be(:component_main) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') }
- let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') }
-
- let_it_be(:architecture_all) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
- let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
- let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
-
- let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated
- let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed
- let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation
- let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation
- let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation
- let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago
-
- def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content)
- component_file = distribution
- .component_files
- .with_component_name(component_name)
- .with_file_type(component_file_type)
- .with_architecture_name(architecture_name)
- .order_updated_asc
- .last
-
- expect(component_file).not_to be_nil
- expect(component_file.updated_at).to eq(release_date)
-
- unless expected_content.nil?
- component_file.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_content)
- end
+ def check_release_files(expected_release_content)
+ distribution.reload
+
+ distribution.file.use_file do |file_path|
+ expect(File.read(file_path)).to eq(expected_release_content)
+ end
+
+ expect(distribution.file_signature).to start_with("-----BEGIN PGP SIGNATURE-----\n")
+ expect(distribution.file_signature).to end_with("\n-----END PGP SIGNATURE-----\n")
+
+ distribution.signed_file.use_file do |file_path|
+ expect(File.read(file_path)).to start_with("-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n#{expected_release_content}-----BEGIN PGP SIGNATURE-----\n")
+ expect(File.read(file_path)).to end_with("\n-----END PGP SIGNATURE-----\n")
end
end
- it 'generates Debian distribution and component files', :aggregate_failures do
- current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456)
-
- travel_to(current_time) do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- initial_count = 6
- destroyed_count = 2
- # updated_count = 1
- created_count = 5
-
- expect { subject }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and change { distribution.reload.updated_at }.to(current_time.round)
- .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count)
- .and change { component_file1.reload.updated_at }.to(current_time.round)
-
- debs = package.package_files.with_debian_file_type(:deb).preload_debian_file_metadata.to_a
- pool_prefix = 'pool/unstable'
- pool_prefix += "/#{project.id}" if container_type == :group
- pool_prefix += "/p/#{package.name}/#{package.version}"
- expected_main_amd64_content = <<~EOF
- Package: libsample0
- Source: #{package.name}
- Version: #{package.version}
- Installed-Size: 7
- Maintainer: #{debs[0].debian_fields['Maintainer']}
- Architecture: amd64
- Description: Some mostly empty lib
- Used in GitLab tests.
- .
- Testing another paragraph.
- Multi-Arch: same
- Homepage: #{debs[0].debian_fields['Homepage']}
- Section: libs
- Priority: optional
- Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb
- Size: 409600
- MD5sum: #{debs[0].file_md5}
- SHA256: #{debs[0].file_sha256}
-
- Package: sample-dev
- Source: #{package.name} (#{package.version})
- Version: 1.2.3~binary
- Installed-Size: 7
- Maintainer: #{debs[1].debian_fields['Maintainer']}
- Architecture: amd64
- Depends: libsample0 (= 1.2.3~binary)
- Description: Some mostly empty development files
- Used in GitLab tests.
- .
- Testing another paragraph.
- Multi-Arch: same
- Homepage: #{debs[1].debian_fields['Homepage']}
- Section: libdevel
- Priority: optional
- Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb
- Size: 409600
- MD5sum: #{debs[1].file_md5}
- SHA256: #{debs[1].file_sha256}
- EOF
-
- check_component_file(current_time.round, 'main', :packages, 'all', nil)
- check_component_file(current_time.round, 'main', :packages, 'amd64', expected_main_amd64_content)
- check_component_file(current_time.round, 'main', :packages, 'arm64', nil)
-
- check_component_file(current_time.round, 'contrib', :packages, 'all', nil)
- check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil)
- check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil)
-
- main_amd64_size = expected_main_amd64_content.length
- main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content)
- main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
-
- contrib_all_size = component_file1.size
- contrib_all_md5sum = component_file1.file_md5
- contrib_all_sha256 = component_file1.file_sha256
-
- expected_release_content = <<~EOF
- Codename: unstable
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
- Architectures: all amd64 arm64
- Components: contrib main
- MD5Sum:
- #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages
- #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages
- SHA256:
- #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
- #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
- EOF
-
- distribution.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_release_content)
+ context 'with Debian components and architectures' do
+ let_it_be(:component_main) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') }
+ let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') }
+
+ let_it_be(:architecture_all) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
+ let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
+ let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
+
+ let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated
+ let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed
+ let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation
+ let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation
+ let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation
+ let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago
+
+ def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content)
+ component_file = distribution
+ .component_files
+ .with_component_name(component_name)
+ .with_file_type(component_file_type)
+ .with_architecture_name(architecture_name)
+ .order_updated_asc
+ .last
+
+ expect(component_file).not_to be_nil
+ expect(component_file.updated_at).to eq(release_date)
+
+ unless expected_content.nil?
+ component_file.file.use_file do |file_path|
+ expect(File.read(file_path)).to eq(expected_content)
+ end
+ end
+ end
+
+ it 'generates Debian distribution and component files', :aggregate_failures do
+ current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456)
+
+ travel_to(current_time) do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ initial_count = 6
+ destroyed_count = 2
+ # updated_count = 1
+ created_count = 5
+
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::PackageFile.count }
+ .and change { distribution.reload.updated_at }.to(current_time.round)
+ .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count)
+ .and change { component_file1.reload.updated_at }.to(current_time.round)
+
+ debs = package.package_files.with_debian_file_type(:deb).preload_debian_file_metadata.to_a
+ pool_prefix = 'pool/unstable'
+ pool_prefix += "/#{project.id}" if container_type == :group
+ pool_prefix += "/p/#{package.name}/#{package.version}"
+ expected_main_amd64_content = <<~EOF
+ Package: libsample0
+ Source: #{package.name}
+ Version: #{package.version}
+ Installed-Size: 7
+ Maintainer: #{debs[0].debian_fields['Maintainer']}
+ Architecture: amd64
+ Description: Some mostly empty lib
+ Used in GitLab tests.
+ .
+ Testing another paragraph.
+ Multi-Arch: same
+ Homepage: #{debs[0].debian_fields['Homepage']}
+ Section: libs
+ Priority: optional
+ Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb
+ Size: 409600
+ MD5sum: #{debs[0].file_md5}
+ SHA256: #{debs[0].file_sha256}
+
+ Package: sample-dev
+ Source: #{package.name} (#{package.version})
+ Version: 1.2.3~binary
+ Installed-Size: 7
+ Maintainer: #{debs[1].debian_fields['Maintainer']}
+ Architecture: amd64
+ Depends: libsample0 (= 1.2.3~binary)
+ Description: Some mostly empty development files
+ Used in GitLab tests.
+ .
+ Testing another paragraph.
+ Multi-Arch: same
+ Homepage: #{debs[1].debian_fields['Homepage']}
+ Section: libdevel
+ Priority: optional
+ Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb
+ Size: 409600
+ MD5sum: #{debs[1].file_md5}
+ SHA256: #{debs[1].file_sha256}
+ EOF
+
+ check_component_file(current_time.round, 'main', :packages, 'all', nil)
+ check_component_file(current_time.round, 'main', :packages, 'amd64', expected_main_amd64_content)
+ check_component_file(current_time.round, 'main', :packages, 'arm64', nil)
+
+ check_component_file(current_time.round, 'contrib', :packages, 'all', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil)
+
+ main_amd64_size = expected_main_amd64_content.length
+ main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content)
+ main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
+
+ contrib_all_size = component_file1.size
+ contrib_all_md5sum = component_file1.file_md5
+ contrib_all_sha256 = component_file1.file_sha256
+
+ expected_release_content = <<~EOF
+ Codename: unstable
+ Date: Sat, 25 Jan 2020 15:17:18 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ Architectures: all amd64 arm64
+ Components: contrib main
+ MD5Sum:
+ #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages
+ #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages
+ SHA256:
+ #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
+ #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
+ EOF
+
+ check_release_files(expected_release_content)
end
end
end
-end
-RSpec.shared_examples 'Generate minimal Debian Distribution' do
- it 'generates minimal distribution', :aggregate_failures do
- travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- expect { subject }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and not_change { distribution.component_files.reset.count }
-
- expected_release_content = <<~EOF
- Codename: unstable
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
- MD5Sum:
- SHA256:
- EOF
-
- distribution.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_release_content)
+ context 'without components and architectures' do
+ it 'generates minimal distribution', :aggregate_failures do
+ travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::PackageFile.count }
+ .and not_change { distribution.component_files.reset.count }
+
+ expected_release_content = <<~EOF
+ Codename: unstable
+ Date: Sat, 25 Jan 2020 15:17:18 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ MD5Sum:
+ SHA256:
+ EOF
+
+ check_release_files(expected_release_content)
end
end
end
diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
index b34a2450af8..c00c3efe6d6 100644
--- a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
@@ -65,4 +65,18 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
expect(rendered).to have_link('Milestones', href: group_milestones_path(group))
end
end
+
+ describe 'Merge Requests' do
+ it 'has a link to the merge request list path' do
+ render
+
+ expect(rendered).to have_link('Merge requests', href: merge_requests_group_path(group))
+ end
+
+ it 'shows pill with the number of merge requests' do
+ render
+
+ expect(rendered).to have_css('span.badge.badge-pill.merge_counter.js-merge-counter')
+ end
+ end
end